探討了面向對象編程(OOP)的一些根本性問題深入理解:
標題:What is so wrong with OOP?
什么是面向對象的問題?
這不是說 OOP “絕對錯誤”,而是指出它在實踐中經常引發的問題,尤其是在性能敏感或復雜系統中。
“OOP marries data with operations… it’s not a happy marriage”
面向對象把數據和操作“捆綁在一起”……但這場婚姻并不幸福。
在 OOP 中,我們將數據(字段)和操作(方法)封裝在一個“對象”中。這種捆綁設計雖然有時有用,但會導致下面的問題。
“Heterogeneous data is brought together by a ‘logical’ black box object”
各種不同類型的數據被強行聚合在一個“邏輯上”的黑箱對象中
OOP 鼓勵我們創建包含各種成員變量的大對象,即使這些成員之間關系松散或不一致。
這會導致:
- 內存布局不友好
- 數據訪問缺乏局部性
- 不同關注點混雜在一起
“The object is used in vastly different contexts”
同一個對象被用于完全不同的上下文中
由于對象通常包含許多功能,它會在多種用途中被“過度使用”:
- 違反單一職責原則
- 難以理解和維護
- 引發 “上帝對象”(God Object) 問題
“Hides state all over the place”
狀態被隱藏在各個地方
OOP 對象內部維護狀態,使用者通常只能通過接口訪問。但:
- 狀態流動不清晰
- 易出現副作用
- 導致調試困難和不可預測行為
- 使多線程編程更復雜
Impact on:
OOP 的這些問題帶來的直接后果包括:
屬性 | 問題 |
---|---|
Performance(性能) | 數據布局差、內存碎片、虛函數開銷 |
Scalability(可擴展性) | 類體系臃腫、繼承鏈過長 |
Modifiability(可維護性) | 高耦合、修改一個類可能影響多個模塊 |
Testability(可測試性) | 隱藏狀態+副作用使得單元測試難寫、難斷言 |
總結:為什么要警惕傳統 OOP?
傳統 OOP 的核心問題:
- “一刀切式”的設計模式(如繼承)
- 濫用封裝,導致信息隱藏過度
- 強耦合與低內聚
- 面向過程的問題被包裝成對象后依然存在
替代思路
現代 C++ 社區開始傾向:
- 組合優于繼承(Composition over Inheritance)
- 使用 值語義對象(value semantics)
- 使用 函數式風格(純函數、不可變狀態)
- 數據結構和算法解耦(數據專注存儲,算法專注變換)
- 面向組件/模塊化架構
關于 Data-Oriented Design (DOD) 的總結非常精煉,下面我幫你詳細拆解理解:
1. Separates data from logic(數據和邏輯分離)
- Structs and functions live independent lives
數據結構(structs)和函數不綁定在一起,不像 OOP 那樣把數據和方法封裝到一個類里。 - Data is regarded as information that has to be transformed
數據被看作純粹的信息,需要被處理和轉換,而不是隱含行為的容器。
2. The logic embraces the data(邏輯“包容”數據)
- Does not try to hide it
邏輯不會去隱藏數據狀態,而是直接操作數據。 - Leads to functions that work on arrays
邏輯往往是針對數組、連續內存塊進行批量操作,而非單個對象。
3. Reorganizes data according to its usage(根據使用場景重組數據)
- If we aren’t going to use a piece of information, why pack it together?
只有程序運行時真正用到的數據才會被聚合。
不會像 OOP 把所有字段都塞進一個對象,避免無用數據影響性能。
4. Avoids “hidden state”(避免隱藏狀態)
- 狀態是顯式、清晰的,沒有隱藏的成員變量或復雜的內部狀態。
- 這樣易于理解、調試和并行處理。
5. No virtual calls(沒有虛函數調用)
- 沒有繼承、多態,不用虛函數表(vtable)
- 因為數據和邏輯分離,邏輯函數獨立存在,調用開銷低,性能更好。
6. Promotes deep domain knowledge(促進深入的領域知識)
- 設計者需要清晰了解數據如何被使用、如何轉換。
- 代碼體現的正是對數據和系統的深刻理解,而不是抽象隱藏。
7. References at the end for more detail(文末有詳細參考資料)
- 這點說明這是個概念總結,實際實踐可以參考具體文獻或項目。
簡單歸納:
DOD 是一種關注 數據結構布局和操作方式 的編程范式,
它摒棄 OOP 中“對象封裝”、“虛函數多態”等設計,更關注 性能和數據訪問效率,
適合需要大量數據處理和高性能需求的領域,比如游戲引擎、嵌入式系統、科學計算等。
關于 CSS 動畫(CSS Animation) 的內容,
CSS Animation 是什么?
1. 動畫定義示例
@keyframes example {from { left: 0px; }to { left: 100px; }
}
div {width: 100px;height: 100px;background-color: red;animation-name: example;animation-duration: 1s;
}
- 直觀聲明(Straightforward declaration)
你只需聲明動畫的關鍵幀(keyframes),CSS 會在一段時間內(animation-duration)平滑地插值(interpolate)某些屬性,從而完成動畫。 - 比如上面動畫就是讓元素從左邊
0px
移動到100px
。
2. 深入觀察(However at a second glance…)
- 屬性類型多樣
動畫不僅僅是數字(像left
),也有顏色、透明度等各種屬性,處理方式復雜多樣。 - 背后有 DOM API 支持
瀏覽器提供了 JavaScript API,比如Animation
、KeyframeEffect
等類,來操作和控制動畫。
這說明 CSS 動畫背后有一套對象模型和邏輯支持,而不是單純的靜態樣式聲明。
總結
- CSS 動畫表面看起來簡單,聲明式,易用;
- 但實現上涉及多種不同數據類型的插值算法;
- 還有配套的 DOM 動畫 API,支持更靈活的控制和管理動畫。
Chromium(Google 瀏覽器內核)中 Blink 動畫系統的 OOP 設計,幫你總結和理解一下:
Chromium Blink 動畫系統的 OOP 設計特點
- 兩個動畫系統
Chromium 實際上有兩個動畫系統,這里關注的是 Blink 動畫系統。 - 經典面向對象設計
- 代碼大量用到繼承和接口(多重繼承),符合傳統 OOP 風格。
- 類名上體現出
final
(表示類不可繼承),說明設計時對繼承做了限制。 - 繼承鏈長,接口多,職責分散在不同的類和接口上。
- 符合 HTML5 標準和 IDL
Blink 動畫設計嚴格遵循 HTML5 的動畫規范和接口定義語言(IDL),以保證標準兼容。 - 動畫對象設計
- 運行中的動畫被抽象成獨立的對象。
- 每個動畫對象負責自身的狀態和行為,利用面向對象的封裝和多態。
- 類聲明示例
class CORE_EXPORT Animation final: public EventTargetWithInlineData,public ActiveScriptWrappable<Animation>,public ContextLifecycleObserver,public CompositorAnimationDelegate,public CompositorAnimationClient,public AnimationEffectOwner {// ...
};
- 這個類繼承了多個接口/基類,表明它承擔了事件目標、腳本包裝、生命周期管理、合成動畫代理等多種職責。
理解點總結
- Blink動畫系統用 OOP 分離職責,設計復雜且功能豐富。
- 多重繼承使得動畫對象能同時具備多種能力。
- 這種設計模式便于功能擴展和維護,但也可能帶來類之間耦合度高、理解難度大的問題。
- 適合大項目和標準實現,符合大型瀏覽器架構需求。
涉及了 Chromium Blink 動畫系統中動畫更新的流程、狀態管理、性能開銷和設計復雜性。我幫你分塊詳細分析,并結合你給的代碼和描述,做深入理解和說明:
1. 代碼流程(ServiceAnimations)
void DocumentTimeline::ServiceAnimations(TimingUpdateReason reason) {TRACE_EVENT("blink", "DocumentTimeline::serviceAnimations");last_current_time_internal_ = CurrentTimeInternal();HeapVector<Member<Animation>> animations;animations.ReserveInitialCapacity(animations_needing_update_.size());for (Animation* animation : animations_needing_update_) {animations.push_back(animation);}std::sort(animations.begin(), animations.end(), Animation::HasLowerPriority);for (Animation* animation : animations) {if (!animation->Update(reason)) {animations_needing_update_.erase(animation);}}
}
主要點:
- last_current_time_internal_:當前時間戳更新,用于同步動畫時間。
- animations_needing_update_ 是動畫集合,這里復制到局部
animations
,防止在遍歷中修改容器出錯。 - 排序:調用
Animation::HasLowerPriority
排序,確保動畫根據優先級執行更新。 - 更新:遍歷動畫,調用
animation->Update(reason)
,若返回 false 代表該動畫不再需要更新,則從animations_needing_update_
移除。
問題點:
animations_needing_update_
使用裸指針,生命周期不清晰,容易出現懸空指針。HeapVector<Member<Animation>>
使用了 Blink 的 GC 智能指針管理,但裸指針和智能指針混用復雜且易錯。- 遍歷時刪除元素的安全性需要保證(這里用副本繞開了修改遍歷容器的問題)。
- 多線程與異步情況下,狀態同步和內存安全隱患較大。
2. Animation::Update() 的狀態管理和性能問題
bool Animation::Update(TimingUpdateReason reason) {if (!timeline)return false;PlayStateUpdateScope update_scope(*this, reason, kDoNotSetCompositorPending);ClearOutdated();bool idle = PlayStateInternal() == kIdle;double inherited_time = idle || IsNull(timeline_->CurrentTimeInternal())? NullValue(): CurrentTimeInternal();// Special case for backwards playback starting at time 0if (inherited_time == 0 && playback_rate_ < 0)inherited_time = -1;if (content_) {content_->UpdateInheritedTime(inherited_time, reason);}// ... 省略后續代碼
}
解析:
- 隱藏狀態:動畫持有復雜狀態
timeline
、播放狀態PlayStateInternal()
、播放速率playback_rate_
等。這些狀態影響更新結果,但不直觀,很容易出錯。 - 分支預測失誤:
if
語句頻繁且依賴運行時數據,CPU 分支預測壓力大,導致性能下降。 - 條件復雜:動畫正向、反向、空閑、時間空值等多種情況處理,代碼復雜度高,邏輯易混亂。
3. 動畫效果 KeyframeEffect
Member<AnimationEffectReadOnly> content_;
Member<DocumentTimeline> timeline_;
content_
持有動畫效果,負責更新動畫實際數值。timeline_
用于獲取當前時間,控制動畫時間流逝。
4. 性能影響:緩存未命中、上下文切換
- 動畫系統頻繁調用虛函數、動態類型擦除、復雜繼承體系,導致 CPU 數據和指令緩存未命中。
- 多個子系統(動畫、事件、樣式計算)耦合,頻繁跳轉不同模塊,增加緩存壓力。
- **跳轉上下文(context switches)**影響流水線和預測,增加延遲。
- 隱藏狀態和多分支條件進一步影響分支預測效率。
5. 事件與動畫的耦合
if (reason == kTimingUpdateForAnimationFrame &&(!owner_ || owner_->IsEventDispatchAllowed())) {if (event_delegate_)event_delegate_->OnEventCondition(*this);if (needs_update)UpdateChildrenAndEffects();calculated_.time_to_forwards_effect_change =CalculateTimeToEffectChange(true, local_time, time_to_next_iteration);calculated_.time_to_reverse_effect_change =CalculateTimeToEffectChange(false, local_time, time_to_next_iteration);
}
- 動畫更新不僅僅是時間計算,還要觸發事件回調、更新子動畫、計算下一次時間變化。
- 這導致系統耦合度高,動畫邏輯和事件系統緊密綁定,維護難度增加。
6. 插值操作(Interpolate)
class Interpolation : public RefCounted<Interpolation> {
public:virtual ~Interpolation() = default;virtual void Interpolate(int iteration, double fraction) = 0;
};
- 動畫插值是抽象類,支持不同屬性和類型的插值(顏色、位置、數字等)。
- 動態多態(虛函數)帶來運行時開銷和緩存未命中。
- 需要測試不同具體實現的組合,復雜度高。
7. 作用動畫新值的過程及樣式計算耦合
if (changed) {target_->SetNeedsAnimationStyleRecalc();if (RuntimeEnabledFeatures::WebAnimationsSVGEnabled() &&target_->IsSVGElement())ToSVGElement(*target_).SetWebAnimationsPending();
}
- 動畫更新后,目標元素的樣式需要重新計算。
- 觸發樣式變更會一路向上遍歷 DOM 樹,進一步增加計算開銷。
- 動畫和樣式系統耦合,導致性能瓶頸和狀態管理復雜。
8. 總結
設計復雜性和性能挑戰
- 生命周期管理混亂:裸指針與智能指針混用,內存安全風險。
- 隱藏狀態多:狀態分散在多個對象,邏輯分支多且復雜。
- 高耦合度:動畫、事件、樣式緊密耦合,維護和擴展成本高。
- 性能瓶頸:分支預測失敗、緩存未命中、頻繁上下文切換,導致 CPU 效率低。
- 抽象層次多:多重繼承、多態虛函數影響運行效率。
Blink 動畫系統體現了現代瀏覽器復雜而高性能的需求,但也帶來了設計和維護的巨大挑戰。
整理一份基于片段,帶詳細注釋的完整示例代碼,模擬了 Blink 中 DocumentTimeline::ServiceAnimations
和 Animation::Update
的核心流程。代碼寫得簡潔易懂,重點標注設計細節和性能相關點。
#include <vector>
#include <algorithm>
#include <iostream>
// Blink 中的 Member 智能指針簡化版,實際是裸指針
template <typename T>
using Member = T*;
// 模擬動畫更新時間的原因類型
using TimingUpdateReason = int;
constexpr TimingUpdateReason kTimingUpdateForAnimationFrame = 1;
// 先聲明 Animation,避免后面 DocumentTimeline 和 Animation 循環依賴
class Animation;
// 時間軸類,管理當前時間和需要更新的動畫列表
class DocumentTimeline {
public:DocumentTimeline() : last_current_time_internal_(nullptr) {}// 獲取當前時間的指針(模擬,固定返回 current_time_ 地址)double* CurrentTimeInternal() { return ¤t_time_; }// 更新所有需要服務(刷新的)動畫void ServiceAnimations(TimingUpdateReason reason);// 添加動畫到時間軸,并設置動畫的時間軸指針void AddAnimation(Animation* animation);
private:// 從需要更新的動畫列表中刪除一個動畫void EraseAnimation(Animation* animation);// 需要刷新的動畫集合std::vector<Animation*> animations_needing_update_;// 上一次內部時間指針,調試或記錄用途double* last_current_time_internal_;// 當前時間,模擬為10秒double current_time_ = 10.0;
};
// 播放狀態枚舉,表示動畫是否空閑或正在播放
enum PlayState { kIdle, kRunning };
// 動畫效果類,負責更新關鍵幀時間等
class AnimationEffectReadOnly {
public:void UpdateInheritedTime(double inherited_time, TimingUpdateReason reason) {std::cout << "UpdateInheritedTime called with time: " << inherited_time << "\n";}
};
// 動畫類,包含播放速率、狀態、動畫效果和時間軸指針
class Animation {
public:Animation(): content_(new AnimationEffectReadOnly),timeline_(nullptr),playback_rate_(1.0),state_(kRunning) {}// 更新動畫狀態和時間,返回是否繼續需要更新bool Update(TimingUpdateReason reason) {if (!timeline_) return false; // 沒有時間軸,直接不更新PlayStateUpdateScope update_scope(*this, reason); // 進入播放狀態更新范圍ClearOutdated(); // 清理過時狀態(模擬)bool idle = PlayStateInternal() == kIdle;// 計算繼承時間,如果空閑或者時間軸當前時間為空則用 NullValue()double inherited_time = (idle || timeline_->CurrentTimeInternal() == nullptr)? NullValue(): *timeline_->CurrentTimeInternal();// 反向播放時,當前時間為0特殊處理if (inherited_time == 0 && playback_rate_ < 0) inherited_time = -1;if (content_) {// 更新動畫效果繼承時間content_->UpdateInheritedTime(inherited_time, reason);}return true; // 假設動畫繼續需要更新}void SetTimeline(DocumentTimeline* timeline) { timeline_ = timeline; }void SetPlaybackRate(double rate) { playback_rate_ = rate; }void SetState(PlayState state) { state_ = state; }// 動畫優先級比較(示例總返回false)static bool HasLowerPriority(Animation* a, Animation* b) { return false; }
private:PlayState PlayStateInternal() const { return state_; }void ClearOutdated() {// 清理過時動畫狀態,空實現模擬}double NullValue() const { return -9999; } // 模擬“無效”時間值// 播放狀態更新輔助作用域,構造析構時可管理狀態struct PlayStateUpdateScope {PlayStateUpdateScope(Animation&, TimingUpdateReason) {}~PlayStateUpdateScope() {}};Member<AnimationEffectReadOnly> content_; // 動畫效果內容指針Member<DocumentTimeline> timeline_; // 關聯的時間軸指針double playback_rate_; // 播放速度,正負表示方向PlayState state_; // 播放狀態
};
// DocumentTimeline成員函數實現
void DocumentTimeline::ServiceAnimations(TimingUpdateReason reason) {std::cout << "ServiceAnimations called\n";// 記錄當前時間指針(模擬)last_current_time_internal_ = CurrentTimeInternal();// 復制動畫列表,避免遍歷時修改原始列表引起錯誤std::vector<Animation*> animations;animations.reserve(animations_needing_update_.size());for (Animation* animation : animations_needing_update_) {animations.push_back(animation);}// 對動畫排序(此處總是false,不改變順序)std::sort(animations.begin(), animations.end(), Animation::HasLowerPriority);// 更新每個動畫,更新失敗則從需要更新列表中移除for (Animation* animation : animations) {if (!animation->Update(reason)) {EraseAnimation(animation);}}
}
void DocumentTimeline::AddAnimation(Animation* animation) {animations_needing_update_.push_back(animation);animation->SetTimeline(this);
}
void DocumentTimeline::EraseAnimation(Animation* animation) {auto it =std::find(animations_needing_update_.begin(), animations_needing_update_.end(), animation);if (it != animations_needing_update_.end()) animations_needing_update_.erase(it);
}
// 測試用主函數,創建時間軸和3個動畫,調用服務刷新
int main() {DocumentTimeline timeline;Animation anim1, anim2, anim3;anim1.SetPlaybackRate(1.0);anim2.SetPlaybackRate(-1.0);anim3.SetPlaybackRate(0.5);timeline.AddAnimation(&anim1);timeline.AddAnimation(&anim2);timeline.AddAnimation(&anim3);timeline.ServiceAnimations(kTimingUpdateForAnimationFrame);return 0;
}
代碼說明
DocumentTimeline
維護一個動畫列表animations_needing_update_
,每次調用ServiceAnimations
會復制到局部數組,排序后依次調用動畫的Update
。Animation::Update
會根據播放狀態、時間軸當前時間等條件決定動畫是否繼續運行,并更新動畫效果。- 動畫效果由
AnimationEffectReadOnly
負責,UpdateInheritedTime
模擬動畫關鍵幀的時間更新。 - 播放狀態用枚舉表示,播放速率影響時間計算(正向/反向)。
- 代碼中大量用注釋標注了關鍵設計點和性能隱患,例如隱藏狀態、多態調用、生命周期管理等。
這段示例體現了:
- 生命周期模糊:動畫與時間軸相互持有指針,需小心管理。
- 隱藏狀態影響復雜:播放狀態、時間空值、速率特殊情況。
- 性能折中:復制動畫列表防止遍歷時修改,帶來開銷。
- 耦合性:動畫依賴時間軸和動畫效果,更新過程涉及多個對象。
回顧總結
1. 使用了超過6個復雜的類
- 這個系統涉及很多復雜的類,比如
Animation
(動畫),DocumentTimeline
(時間軸),AnimationEffectReadOnly
(只讀動畫效果)等。 - 每個類負責動畫系統的不同職責,比如時間管理、動畫狀態控制、屬性更新等。
2. 對象內部包含指向其他對象的智能指針
- 這里用的是類似智能指針的機制(在實際瀏覽器中通常是更復雜的引用計數指針)。
- 例如,一個
Animation
對象持有它的AnimationEffectReadOnly
和DocumentTimeline
的智能指針。 - 這樣避免了手動內存管理,防止懸空指針和內存泄漏問題。
3. 插值(Interpolation)使用抽象類來處理不同類型的屬性
- 動畫需要對不同類型的屬性進行插值,比如數字型(位置)、顏色、變換矩陣等。
- 通過定義一個抽象基類接口,具體的屬性類型繼承這個接口,實現對應的插值算法。
- 這樣設計方便擴展新類型屬性,符合開閉原則(不修改已有代碼的情況下新增功能)。
4. CSS 動畫直接調用其他系統,產生耦合
- CSS動畫并不是孤立的,它會直接與瀏覽器的其他子系統交互:
- 事件系統(比如觸發動畫開始、完成的事件)
- DOM元素的樣式設置(把動畫結果直接寫到元素上)
- 這種設計帶來了耦合:
- 不同系統之間相互依賴,改動一個系統可能會影響動畫代碼。
- 也增加了系統整體的復雜性。
5. 元素生命周期如何同步?
- 因為動畫會直接操作 DOM 元素,必須保證動畫和元素的生命周期保持一致。
- 如果元素被銷毀,但動畫還在持有它的指針,就會出現崩潰或未定義行為。
- 瀏覽器通常采用引用計數或者觀察者模式來同步:
- 動畫持有元素的智能指針或弱指針。
- 元素銷毀時通知動畫,動畫釋放對元素的引用。
- 這樣保證動畫不會“越界”訪問已銷毀的元素。
簡單總結:
- 動畫系統設計復雜,涉及多個交互類。
- 智能指針機制幫助管理對象生命周期。
- 抽象類使得不同屬性類型的插值實現靈活可擴展。
- CSS 動畫和瀏覽器其他系統(事件、DOM樣式)耦合較緊。
- 元素和動畫的生命周期管理至關重要,防止崩潰和錯誤。
關于數據導向設計(Data-Oriented Design, DOD)內容的中文解釋:
回到設計白板 — 數據導向設計視角
1. 動畫數據操作(Animation data operations)
- Tick (Update) -> 99.9%
絕大部分時間花在“滴答”更新動畫狀態,即計算動畫當前應該顯示什么樣子。 - Add
添加動畫。 - Remove
刪除動畫。 - Pause
暫停動畫。 - …
其他對動畫生命周期的操作。
這里意味著更新動畫狀態是最關鍵的熱點,其他操作相對較少。
2. 動畫滴答輸入(Animation Tick Input)
- Animation definition(動畫定義)
描述動畫本身(關鍵幀、時長、緩動等)。 - Time(時間)
當前時間或者時間進度,決定動畫播放到哪個階段。
3. 動畫滴答輸出(Animation Tick Output)
- Changed properties(改變了哪些屬性)
哪些 CSS 屬性或者動畫屬性在本次更新中變化了。 - New property values(新屬性值)
動畫計算后得出的屬性最新值。 - Who owns the new values(新值的歸屬)
新計算出的值存在哪里,誰負責管理它們(可能是動畫系統、渲染系統或者 DOM 元素)。
4. 為大量動畫設計(Design for many animations)
- 設計時需要考慮到如何高效地處理成千上萬的動畫實例。
- 數據布局和訪問模式對性能影響巨大。
- 目標是減少緩存未命中(cache misses)、避免復雜的指針追蹤和虛函數調用,利用連續內存和批量處理提升效率。
數據導向設計的核心思想
- 把數據結構設計成適合硬件緩存訪問的形態(例如數組、結構體數組等),而不是以傳統面向對象的方式“圍繞對象設計”。
- 關注“數據流”和“數據轉換”而非“對象之間的關系”。
- 動畫系統中,絕大多數時間花費在每幀更新計算上,所以優化這部分最重要。
下面是給出的 AnimationStateCommon
結構體代碼片段,我幫你補全合理的注釋并做詳細分析。
// 表示動畫狀態的通用結構體,扁平化設計提高緩存效率和訪問速度
struct AnimationStateCommon {AnimationId Id; // 動畫的唯一ID,區分不同動畫實例// 動畫的時間點,基于單調時鐘(mono_clock)mono_clock::time_point::seconds StartTime; // 動畫開始時間點(絕對時間)mono_clock::time_point::seconds PauseTime; // 動畫暫停時的時間點mono_clock::duration::seconds Duration; // 動畫持續時間長度mono_clock::duration::seconds Delay; // 動畫開始前的延遲時間Optional<mono_clock::time_point::seconds> ScheduledPauseTime; // 計劃暫停時間,可選,表示動畫將來某時刻暫停的時間點(如果有)AnimationIterationCount::Value Iterations; // 動畫的循環次數,可能是無限循環AnimationFillMode::Type FillMode; // 填充模式,決定動畫結束后的樣式是否保持AnimationDirection::Type Direction; // 播放方向,比如正向、反向、交替播放等AnimationTimingFunction::Timing Timing; // 時間函數,決定動畫的節奏(緩動函數)AnimationPlayState::Type PlayState; // 動畫當前播放狀態(播放、暫停、停止等)float IterationsPassed = 0.f; // 當前已經播放的迭代次數(包括小數部分,表示播放進度)float PlaybackRate = 1.0f; // 播放速度倍率,1.0為正常速度,<1為慢,>1為快
};
代碼分析
- 結構體作用
這是一個用于保存單個動畫狀態的扁平化數據結構,所有動畫相關的時間點、播放控制和狀態數據都集中在這里。避免分散在多個類或對象中,利于高效批量處理。 - 時間點字段
StartTime
和PauseTime
精確記錄動畫的起始和暫停時間,便于計算當前播放進度。Duration
和Delay
明確動畫持續時間和延遲時間,有助于計算動畫何時真正開始和結束。ScheduledPauseTime
是可選的,支持計劃中的暫停操作,比如動畫運行到某時間自動暫停。
- 播放行為控制
Iterations
支持循環播放,能控制動畫重復次數或無限循環。FillMode
控制動畫結束后元素是否保持動畫結束狀態,常見的如“forwards”、“backwards”等。Direction
支持多種播放方向,方便做來回動畫或交替播放。Timing
是緩動函數,決定動畫進度變化曲線,影響動畫的視覺節奏。
- 播放狀態和進度
PlayState
顯示當前動畫是播放中還是暫停或停止。IterationsPassed
精確表示動畫已播放的進度(可細分到小數),便于精細控制動畫播放。PlaybackRate
允許加速或減速播放,支持更靈活的動畫效果。
- 設計理念
- 通過扁平結構避免復雜指針引用和對象嵌套,提升數據訪問的局部性(cache locality)。
- 結構體中只包含數據,沒有行為,符合數據導向設計思想,方便用數據驅動的批量動畫更新和調度。
額外說明
mono_clock
是模擬的單調時鐘類型,保證時間值不會倒退,適合動畫計時。- 具體枚舉類型(
AnimationFillMode::Type
、AnimationDirection::Type
等)會定義動畫行為的細節規范。 Optional
表示字段可能為空,靈活管理可選狀態。
代碼片段理解與分析
template<typename T>
struct AnimationStateProperty : public AnimationState {AnimatedDefinitionFrames<T> Keyframes;
};
- 這是一個模板結構體
AnimationStateProperty
,繼承自AnimationState
(假設是一個基礎動畫狀態結構)。 - 它針對動畫中具體的屬性類型
T
(例如邊框寬度、Z-index 等)來定義動畫狀態。 - 結構體內部維護了一個
Keyframes
成員,類型為AnimatedDefinitionFrames<T>
,表示這個屬性動畫的關鍵幀數據集合。
關鍵點:避免類型擦除(Avoid type erasure)
- 什么是類型擦除?
類型擦除是一種設計模式,比如用基類指針或接口隱藏具體類型信息,以實現多態。缺點是運行時會丟失具體類型信息,可能導致效率降低,且難以在編譯時進行優化。 - 為什么要避免?
動畫系統性能關鍵,尤其是瀏覽器動畫,每幀都要高效處理大量屬性的動畫。如果使用類型擦除,所有屬性動畫都會變成統一接口,運行時動態分發,開銷大,難以做靜態優化。 - 解決方案
這里采用模板+編譯時類型信息,即針對每種屬性類型T
單獨生成對應的AnimationStateProperty<T>
,可以利用編譯器優化。
這種方式稱為**“靜態多態”**,比運行時多態更高效。
結合實例說明
// 針對不同CSS屬性生成對應的動畫狀態數組(vector):
CSSVector<AnimationStateProperty<BorderWidth>> m_BorderTopWidthActiveAnimState;
CSSVector<AnimationStateProperty<BorderWidth>> m_BorderLeftWidthActiveAnimState;
CSSVector<AnimationStateProperty<ZIndex>> m_ZIndexActiveAnimState;
- 這里
CSSVector
是存儲特定類型動畫狀態的容器,可能是專門優化過的數組或向量。 - 每個成員變量都代表一個具體屬性的動畫狀態集合,且類型已知,全部在編譯期確定。
總結
- 利用模板和靜態類型避免了類型擦除帶來的性能損失。
- 每個屬性動畫狀態都有自己專門的類型,便于針對不同屬性做特化優化。
- 這些容器(
CSSVector
)的聲明由工具自動生成,保證所有需要的屬性動畫都有對應的狀態存儲。 - 整體思路符合數據導向設計(Data-Oriented Design),讓動畫系統高效且類型安全。
這段內容主要在講“動畫的Tick(更新)機制”,以及如何通過模板函數遍歷和更新不同類型的動畫狀態:
核心內容解析
1. 動畫Tick操作
- 動畫系統中,每一幀(frame)都會調用
Tick
函數,來更新所有正在運行的動畫狀態(例如屬性值隨時間的變化)。 - 這里提到“Iterate over all vectors”,意思是動畫系統會遍歷存儲不同屬性動畫狀態的所有容器(例如:
AnimationState<BorderLeft>
的數組、AnimationState<Opacity>
的數組、AnimationState<Transform>
的數組等)。 - 通過遍歷這些數組,逐個調用對應屬性的動畫更新邏輯。
2. 模板實現細節
template<css::PropertyTypes PropType>
AnimationRunningState TickAnimation(mono_clock::time_point::seconds now,AnimationStateProperty<typename css::PropertyValue<PropType>::type_t>& state)
{// 這里是針對某個具體屬性的動畫狀態的更新實現// 例如根據當前時間 now,計算動畫進度,更新動畫狀態中的屬性值
}
- 這是一個模板函數,針對不同的CSS屬性類型
PropType
實現動畫更新(tick)的具體邏輯。 PropType
是一個編譯時常量,代表具體的CSS屬性類型(如BorderLeft
,Opacity
,Transform
等)。AnimationStateProperty<typename css::PropertyValue<PropType>::type_t>
是該屬性對應的動畫狀態類型,模板解析出具體的值類型(如長度、浮點數、矩陣等)。now
是當前時間,用于計算動畫進度和當前動畫狀態。
3. 實現層模板放在.cpp
文件中
- 動畫更新函數通常是實現細節,會放在
.cpp
文件里,用模板實例化的方式編譯。 - 這樣做避免了模板代碼膨脹,也把實現細節隱藏,保持接口清晰。
總結
- 遍歷所有動畫狀態容器,對每種屬性類型的動畫狀態進行更新,保證動畫隨時間正確播放。
- 采用模板函數實現屬性類型的動畫更新,保證代碼復用和高效靜態類型檢查。
- 通過傳入當前時間
now
,計算當前動畫進度,更新屬性值。 - 模板函數放在實現文件,避免模板膨脹,保持代碼整潔。
如何在動畫系統設計中避免條件分支(if判斷),提升性能的思路,尤其是在數據導向設計(DoD)中常用的優化手段。下面是詳細中文理解:
核心內容解析
1. 根據布爾狀態劃分列表
- 將動畫按照某個布爾“標志”(flag)分類,分別存儲在不同的容器(列表)中。
- 這種設計類似數據庫中的表格,按照某個字段進行分組,方便批量操作。
- 在DoD(數據導向設計)里,這種做法很常見,能提高CPU緩存效率和減少條件判斷。
2. 分離活躍動畫和非活躍動畫
- 活躍動畫(Active):當前正在運行的動畫,CPU需要對它們頻繁調用更新(Tick)。
- 雖然活躍動畫是運行中,但可能會被API暫停或停止。
- 非活躍動畫(Inactive):已經完成或暫停的動畫,暫時不需要更新。
- 但它們可以通過API再次啟動。
通過把兩種動畫分開存儲,就避免了在更新時對每個動畫執行if (isActive)
的判斷。
- 但它們可以通過API再次啟動。
3. 避免“if (isActive)”這樣的分支
- 條件分支會打斷CPU流水線,降低指令執行效率,特別是當分支預測失敗時。
- 分離數據后,批量處理活躍動畫,無需條件判斷,CPU能更好地預測和優化流水線。
4. 不可能對所有布爾標志都做到無分支
- 有時候狀態非常多,沒法全部拆成不同容器。
- 這時優先針對最頻繁判斷且分支預測難的布爾狀態做拆分。
- 以提升整體性能。
總結
- 通過數據分離,將活躍動畫和非活躍動畫分別存儲,避免運行時頻繁判斷狀態。
- 類似數據庫表格的設計思路,提升緩存局部性和批量處理效率。
- 減少分支判斷帶來的性能損失。
- 重點針對對性能影響最大的布爾標志做優化。
代碼片段是動畫系統中“關鍵幀插值與時間推進”的核心邏輯:
template<css::PropertyTypes PropType>
AnimationRunningState TickAnimation(mono_clock::time_point::seconds now,AnimationStateProperty<typename css::PropertyValue<PropType>::type_t>& state)
{using Type = typename css::PropertyValue<PropType>::type_t;AnimationRunningState transition;// 計算動畫當前時間點(進度),基于當前時間和動畫狀態const auto t = CalculateAnimationPoint(now, state, transition);assert(!std::isnan(t)); // 確保計算結果不是NaN// 定義兩個關鍵幀指針,from和to,指示當前插值區間const typename AnimatedDefinitionFrames<Type>::Frame* from = nullptr;const typename AnimatedDefinitionFrames<Type>::Frame* to = nullptr;size_t firstFrameIndex;// 確定當前時間t所處的關鍵幀區間,獲得插值參數(interpolator)auto interpolator = DetermineKeyFrameInterval(t, state, from, to, firstFrameIndex);// 應用緩動函數(easing),調整插值比例interpolator = ApplyEase(interpolator, state.Timing, state.Duration);// 根據插值參數和關鍵幀值計算當前動畫值const auto newValue = GetInterpolatedValue(state.Keyframes,firstFrameIndex,interpolator,from->Value,to->Value);// 將計算出來的新值設置到動畫輸出(比如CSS屬性)state.Output->template SetValue<Type, PropType>(newValue);// 返回動畫當前運行狀態(比如正在運行、已結束等)return transition;
}
關鍵點解釋
- 模板參數 PropType:動畫的屬性類型(如透明度、位置、顏色等)。
- CalculateAnimationPoint:計算動畫當前的歸一化時間點(通常是0到1之間,表示動畫進度)。
- DetermineKeyFrameInterval:確定當前時間對應的關鍵幀區間,找到兩個關鍵幀from和to,用于插值。
- ApplyEase:應用緩動函數,讓動畫變化更自然,比如加速、減速等效果。
- GetInterpolatedValue:基于緩動后的插值比例,計算當前動畫值(屬性值)。
- SetValue:將計算出的動畫值應用到動畫輸出對象(比如DOM元素的樣式)。
- AnimationRunningState:返回動畫當前狀態,供調用者判斷動畫是否繼續。
整體作用
該函數就是**“推進動畫狀態、計算當前動畫屬性值并應用”**的核心步驟。每次動畫幀更新都會調用,確保動畫平滑進行。
背景問題
- 我們想對動畫進行操作,比如播放(play)、暫停(pause)、調整播放速度(playbackRate)等。
- 但動畫本身不是一個傳統的對象,而是“散落在數據結構中的一組數據”。
- 也就是說,沒有一個單獨的“Animation”實例,動畫其實是通過**AnimationId(動畫ID)**來標識和操作。
設計思想
- AnimationId 是一個無符號整數,作為動畫的唯一標識符。
- 對動畫的操作,是通過這個動畫ID去查找和修改對應的數據狀態。
- 在JavaScript接口層,會封裝一個對象,代表動畫,內部持有AnimationId。
- JS調用例如
animation.play()
,實際上會調用底層 C++ 的AnimationController::Play(id)
等接口,傳入AnimationId進行操作。
優勢
- 動畫數據和控制邏輯解耦,數據以“表”形式組織(Data-Oriented Design)。
- 動畫對象輕量,只是一個“句柄”(handle),不用管理復雜對象生命周期。
- 方便批量管理和高效更新。
總結
- Animation = 數據句柄 + API包裝。
- API函數不直接操作對象,而是用 AnimationId 作為操作鍵。
- AnimationController 負責管理所有動畫狀態和執行控制操作。
理解這個設計就是:
設計思路
- AnimationController 是負責所有動畫數據修改和管理的核心類。
- “Animation”對象其實很輕量,只是持有一個
AnimationId
,作為對動畫數據的句柄。 - 所有操作都通過 AnimationController 和 AnimationId 完成,保證數據集中管理,避免狀態分散。
具體API函數解釋
PauseAnimation(AnimationId animationId);
暫停指定ID的動畫。PlayAnimation(AnimationId animationId);
播放指定ID的動畫。PlayFromTo(AnimationId animationId, mono_clock::duration::milliseconds playTime, mono_clock::duration::milliseconds pauseTime);
從playTime
開始播放,到pauseTime
暫停。實現細粒度控制。SetAnimationSeekTime(AnimationId animationId, mono_clock::duration::milliseconds seekTime);
設置動畫的當前播放時間(跳轉到某一幀或時間點)。GetAnimationSeekTime(AnimationId animationId);
獲取動畫當前的播放時間。SetAnimationPlaybackRate(AnimationId animationId, float playbackRate);
設置動畫播放速率(如正常速度、加速或倒播)。GetAnimationPlaybackRate(AnimationId animationId);
獲取動畫當前播放速率。ReverseAnimation(AnimationId animationId);
反轉動畫播放方向。
作用和優勢
- 動畫控制邏輯完全由 AnimationController 管理,便于維護和擴展。
- AnimationId 作為接口的核心,簡化了JS和底層數據結構的交互。
- 通過這種設計,實現了數據驅動的動畫控制,同時保持API層的簡潔易用。
對比了面向對象編程(OOP)和數據導向設計(DoD)在動畫系統中的對應概念,核心思想是展示兩種設計范式如何處理動畫數據和邏輯的不同:
1. 類繼承 vs 模板結構體
- OOP:
blink::Animation
通過繼承6個類來組織行為和狀態(繼承樹復雜)。 - DoD: 使用模板結構體
AnimationState
,把動畫狀態數據按屬性類型分開,減少復雜的繼承和虛函數開銷。
2. 引用 vs 只讀數據副本
- OOP: 動畫中直接引用關鍵幀數據(Keyframe),可能伴隨共享和修改。
- DoD: 使用關鍵幀數據的只讀副本,保證數據訪問的局部性和安全性,方便批量處理。
3. 動態分配的插值器列表 vs 按屬性分開的向量
- OOP: 動態創建不同類型的插值器,管理起來復雜,內存分散。
- DoD: 針對每種屬性(如opacity, transform)有獨立的向量,方便連續訪問和SIMD優化。
4. 以布爾標志控制活躍狀態 vs 按狀態劃分的表
- OOP: 用布爾變量表示動畫是否活躍,代碼中頻繁判斷(分支預測影響性能)。
- DoD: 按是否活躍,把動畫分別存放在不同的表(vectors)中,避免分支,提高緩存友好。
5. 繼承動畫接口 vs 用動畫ID句柄
- OOP: 動畫類繼承
blink::ActiveScriptWrappable
接口,綁定腳本層操作。 - DoD: 用一個簡單的動畫ID(handle)在腳本和數據層之間傳遞,數據操作全由
AnimationController
集中管理。
6. 輸出新屬性值到DOM元素 vs 輸出到表
- OOP: 動畫直接操作DOM元素,設置樣式值,耦合DOM和動畫實現。
- DoD: 動畫先輸出新的屬性值到專門的表(數據結構),由后續系統統一應用到DOM,解耦提高性能。
7. 標記DOM元素層級以觸發樣式刷新 vs 修改元素列表
- OOP: 標記整個DOM樹(元素層級)觸發樣式更新,可能影響范圍大,效率低。
- DoD: 維護一個被修改元素的列表,只處理必要的元素,減少不必要的工作。
總結
- OOP設計更注重對象的抽象和行為封裝,代碼結構復雜,運行時依賴多,分支和動態分配多。
- DoD設計聚焦數據布局和批量操作,減少分支,提升CPU緩存效率,性能更高但代碼更“平坦”,抽象更低。
#include <iostream>
#include <vector>
#include <cmath>
#include <cassert>
#include <unordered_map>
#include <optional>
// ---------------- 通用定義 -------------------
// 秒數類型別名
using Seconds = double;
// 動畫 ID 類型(整型)
using AnimationId = unsigned int;
// 模擬單調時鐘命名空間
namespace mono_clock {using time_point = Seconds;using duration = Seconds;
}
// ---------------- Keyframe 和插值 -------------------
// 表示一個關鍵幀:在某個時間點具有一個特定值
template <typename T>
struct Keyframe {Seconds time;T value;
};
// 包含所有關鍵幀集合
template <typename T>
struct AnimatedDefinitionFrames {using Frame = Keyframe<T>;std::vector<Frame> frames;
};
// 簡單線性插值函數:a + (b - a) * t
template <typename T>
T Interpolate(const T& a, const T& b, double t) {return a + (b - a) * t;
}
// ---------------- 偽 enum 定義 -------------------
// 動畫迭代次數類型(整數)
namespace AnimationIterationCount {using Value = int; // 負數表示無限循環
}
namespace AnimationFillMode {enum Type { None, Forwards, Backwards, Both };
}
namespace AnimationDirection {enum Type { Normal, Reverse };
}
namespace AnimationTimingFunction {enum Timing { Linear, EaseIn, EaseOut }; // 簡化版緩動函數
}
namespace AnimationPlayState {enum Type { Running, Paused, Finished };
}
// ---------------- 動畫狀態結構體 -------------------
// 通用動畫狀態結構體(扁平結構)
struct AnimationStateCommon {AnimationId Id; // 動畫唯一標識符Seconds StartTime; // 動畫啟動時的時間戳Seconds PauseTime; // 暫停時記錄的時間點Seconds Duration; // 動畫持續時間Seconds Delay; // 動畫延遲時間std::optional<Seconds> ScheduledPauseTime; // 預定暫停時間(可選)// 動畫配置AnimationIterationCount::Value Iterations = 1; // 迭代次數AnimationFillMode::Type FillMode = AnimationFillMode::None;AnimationDirection::Type Direction = AnimationDirection::Normal;AnimationTimingFunction::Timing Timing = AnimationTimingFunction::Linear;AnimationPlayState::Type PlayState = AnimationPlayState::Running; // 初始為運行狀態float IterationsPassed = 0.f; // 運行次數(支持小數)float PlaybackRate = 1.0f; // 播放倍率(1.0 = 正常播放)
};
// ---------------- 屬性模板狀態 -------------------
// 用于管理某個屬性(如 opacity)的動畫狀態
template <typename T>
struct AnimationStateProperty : public AnimationStateCommon {AnimatedDefinitionFrames<T> Keyframes; // 當前屬性的關鍵幀T OutputValue; // 插值計算的輸出值(當前幀值)
};
// ---------------- 屬性類型定義(模擬) -------------------
namespace css {enum class PropertyTypes { Opacity, ZIndex };template <PropertyTypes>struct PropertyValue;// 指定 Opacity 對應 floattemplate <>struct PropertyValue<PropertyTypes::Opacity> {using type_t = float;};// 指定 ZIndex 對應 inttemplate <>struct PropertyValue<PropertyTypes::ZIndex> {using type_t = int;};
}
// ---------------- Tick 動畫函數模板 -------------------
// 針對某個屬性(如 Opacity)的狀態進行插值計算
template <css::PropertyTypes PropType>
AnimationPlayState::Type TickAnimation(Seconds now, AnimationStateProperty<typename css::PropertyValue<PropType>::type_t>& state) {using Type = typename css::PropertyValue<PropType>::type_t;// 跳過非運行狀態if (state.PlayState != AnimationPlayState::Running)return state.PlayState;// 根據當前時間和播放速率計算局部時間(相對時間)Seconds local = (now - state.StartTime) * state.PlaybackRate;// 超出動畫時長則標記為已結束if (local < 0 || local > state.Duration) {state.PlayState = AnimationPlayState::Finished;return state.PlayState;}// 若關鍵幀不足 2 幀,無法插值,直接返回auto& frames = state.Keyframes.frames;if (frames.size() < 2) return state.PlayState;// 找到當前時間所處的關鍵幀段size_t idx = 0;while (idx + 1 < frames.size() && local > frames[idx + 1].time)++idx;auto& from = frames[idx];auto& to = frames[idx + 1];// 計算插值比例 tdouble segment = (local - from.time) / (to.time - from.time);// 應用緩動函數if (state.Timing == AnimationTimingFunction::EaseIn)segment = std::pow(segment, 2);else if (state.Timing == AnimationTimingFunction::EaseOut)segment = std::sqrt(segment);// 插值計算結果賦值state.OutputValue = Interpolate(from.value, to.value, segment);return state.PlayState;
}
// ---------------- AnimationController 控制器 -------------------
class AnimationController {
public:// 添加一條 opacity 動畫void AddOpacityAnimation(const AnimationStateProperty<float>& anim) {opacityAnimations[anim.Id] = anim;}// 全部動畫 tick 更新void TickAll(Seconds now) {for (auto& [id, anim] : opacityAnimations) {TickAnimation<css::PropertyTypes::Opacity>(now, anim);std::cout << "Opacity[" << id << "] = " << anim.OutputValue << "\n";}}// 動畫控制接口void PauseAnimation(AnimationId id) {if (auto* a = GetOpacity(id)) a->PlayState = AnimationPlayState::Paused;}void PlayAnimation(AnimationId id) {if (auto* a = GetOpacity(id)) {a->PlayState = AnimationPlayState::Running;a->StartTime = current_time;}}void SetAnimationSeekTime(AnimationId id, Seconds seekTime) {if (auto* a = GetOpacity(id))a->StartTime = current_time - seekTime / a->PlaybackRate;}Seconds GetAnimationSeekTime(AnimationId id) {if (auto* a = GetOpacity(id))return (current_time - a->StartTime) * a->PlaybackRate;return 0;}void SetAnimationPlaybackRate(AnimationId id, float rate) {if (auto* a = GetOpacity(id)) a->PlaybackRate = rate;}float GetAnimationPlaybackRate(AnimationId id) {if (auto* a = GetOpacity(id)) return a->PlaybackRate;return 1.0f;}void ReverseAnimation(AnimationId id) {if (auto* a = GetOpacity(id)) a->PlaybackRate *= -1;}// 模擬時間推進void AdvanceTime(Seconds t) {current_time += t;TickAll(current_time);}
private:Seconds current_time = 0;std::unordered_map<AnimationId, AnimationStateProperty<float>> opacityAnimations;// 獲取指定 ID 的動畫狀態AnimationStateProperty<float>* GetOpacity(AnimationId id) {auto it = opacityAnimations.find(id);return it != opacityAnimations.end() ? &it->second : nullptr;}
};
// ---------------- 主函數 -------------------
int main() {AnimationController controller;// 創建一條 opacity 動畫(ID 為 1)AnimationStateProperty<float> opacity;opacity.Id = 1;opacity.StartTime = 0;opacity.Duration = 2.0;opacity.Keyframes.frames = {{0.0, 0.0f},{1.0, 0.5f},{2.0, 1.0f}};controller.AddOpacityAnimation(opacity);// 模擬每 0.5 秒進行一次 tickfor (int i = 0; i <= 5; ++i) {controller.AdvanceTime(0.5);}std::cout << "Pausing...\n";controller.PauseAnimation(1);controller.AdvanceTime(1.0); // 暫停后不應更新std::cout << "Resuming...\n";controller.PlayAnimation(1);controller.AdvanceTime(1.0); // 恢復后繼續更新return 0;
}
下面是你提供的動畫系統代碼,逐行添加了詳細中文注釋。這是一套符合 Data-Oriented Design(數據導向設計)理念的動畫控制框架,結構清晰、可拓展性強。
完整代碼(帶注釋)
#include <iostream>
#include <vector>
#include <cmath>
#include <cassert>
#include <unordered_map>
#include <optional>
// ---------------- 通用定義 -------------------
// 秒數類型別名
using Seconds = double;
// 動畫 ID 類型(整型)
using AnimationId = unsigned int;
// 模擬單調時鐘命名空間
namespace mono_clock {using time_point = Seconds;using duration = Seconds;
}
// ---------------- Keyframe 和插值 -------------------
// 表示一個關鍵幀:在某個時間點具有一個特定值
template <typename T>
struct Keyframe {Seconds time;T value;
};
// 包含所有關鍵幀集合
template <typename T>
struct AnimatedDefinitionFrames {using Frame = Keyframe<T>;std::vector<Frame> frames;
};
// 簡單線性插值函數:a + (b - a) * t
template <typename T>
T Interpolate(const T& a, const T& b, double t) {return a + (b - a) * t;
}
// ---------------- 偽 enum 定義 -------------------
// 動畫迭代次數類型(整數)
namespace AnimationIterationCount {using Value = int; // 負數表示無限循環
}
namespace AnimationFillMode {enum Type { None, Forwards, Backwards, Both };
}
namespace AnimationDirection {enum Type { Normal, Reverse };
}
namespace AnimationTimingFunction {enum Timing { Linear, EaseIn, EaseOut }; // 簡化版緩動函數
}
namespace AnimationPlayState {enum Type { Running, Paused, Finished };
}
// ---------------- 動畫狀態結構體 -------------------
// 通用動畫狀態結構體(扁平結構)
struct AnimationStateCommon {AnimationId Id; // 動畫唯一標識符Seconds StartTime; // 動畫啟動時的時間戳Seconds PauseTime; // 暫停時記錄的時間點Seconds Duration; // 動畫持續時間Seconds Delay; // 動畫延遲時間std::optional<Seconds> ScheduledPauseTime; // 預定暫停時間(可選)// 動畫配置AnimationIterationCount::Value Iterations = 1; // 迭代次數AnimationFillMode::Type FillMode = AnimationFillMode::None;AnimationDirection::Type Direction = AnimationDirection::Normal;AnimationTimingFunction::Timing Timing = AnimationTimingFunction::Linear;AnimationPlayState::Type PlayState = AnimationPlayState::Running; // 初始為運行狀態float IterationsPassed = 0.f; // 運行次數(支持小數)float PlaybackRate = 1.0f; // 播放倍率(1.0 = 正常播放)
};
// ---------------- 屬性模板狀態 -------------------
// 用于管理某個屬性(如 opacity)的動畫狀態
template <typename T>
struct AnimationStateProperty : public AnimationStateCommon {AnimatedDefinitionFrames<T> Keyframes; // 當前屬性的關鍵幀T OutputValue; // 插值計算的輸出值(當前幀值)
};
// ---------------- 屬性類型定義(模擬) -------------------
namespace css {enum class PropertyTypes { Opacity, ZIndex };template <PropertyTypes>struct PropertyValue;// 指定 Opacity 對應 floattemplate <>struct PropertyValue<PropertyTypes::Opacity> {using type_t = float;};// 指定 ZIndex 對應 inttemplate <>struct PropertyValue<PropertyTypes::ZIndex> {using type_t = int;};
}
// ---------------- Tick 動畫函數模板 -------------------
// 針對某個屬性(如 Opacity)的狀態進行插值計算
template <css::PropertyTypes PropType>
AnimationPlayState::Type TickAnimation(Seconds now, AnimationStateProperty<typename css::PropertyValue<PropType>::type_t>& state) {using Type = typename css::PropertyValue<PropType>::type_t;// 跳過非運行狀態if (state.PlayState != AnimationPlayState::Running)return state.PlayState;// 根據當前時間和播放速率計算局部時間(相對時間)Seconds local = (now - state.StartTime) * state.PlaybackRate;// 超出動畫時長則標記為已結束if (local < 0 || local > state.Duration) {state.PlayState = AnimationPlayState::Finished;return state.PlayState;}// 若關鍵幀不足 2 幀,無法插值,直接返回auto& frames = state.Keyframes.frames;if (frames.size() < 2) return state.PlayState;// 找到當前時間所處的關鍵幀段size_t idx = 0;while (idx + 1 < frames.size() && local > frames[idx + 1].time)++idx;auto& from = frames[idx];auto& to = frames[idx + 1];// 計算插值比例 tdouble segment = (local - from.time) / (to.time - from.time);// 應用緩動函數if (state.Timing == AnimationTimingFunction::EaseIn)segment = std::pow(segment, 2);else if (state.Timing == AnimationTimingFunction::EaseOut)segment = std::sqrt(segment);// 插值計算結果賦值state.OutputValue = Interpolate(from.value, to.value, segment);return state.PlayState;
}
// ---------------- AnimationController 控制器 -------------------
class AnimationController {
public:// 添加一條 opacity 動畫void AddOpacityAnimation(const AnimationStateProperty<float>& anim) {opacityAnimations[anim.Id] = anim;}// 全部動畫 tick 更新void TickAll(Seconds now) {for (auto& [id, anim] : opacityAnimations) {TickAnimation<css::PropertyTypes::Opacity>(now, anim);std::cout << "Opacity[" << id << "] = " << anim.OutputValue << "\n";}}// 動畫控制接口void PauseAnimation(AnimationId id) {if (auto* a = GetOpacity(id)) a->PlayState = AnimationPlayState::Paused;}void PlayAnimation(AnimationId id) {if (auto* a = GetOpacity(id)) {a->PlayState = AnimationPlayState::Running;a->StartTime = current_time;}}void SetAnimationSeekTime(AnimationId id, Seconds seekTime) {if (auto* a = GetOpacity(id))a->StartTime = current_time - seekTime / a->PlaybackRate;}Seconds GetAnimationSeekTime(AnimationId id) {if (auto* a = GetOpacity(id))return (current_time - a->StartTime) * a->PlaybackRate;return 0;}void SetAnimationPlaybackRate(AnimationId id, float rate) {if (auto* a = GetOpacity(id)) a->PlaybackRate = rate;}float GetAnimationPlaybackRate(AnimationId id) {if (auto* a = GetOpacity(id)) return a->PlaybackRate;return 1.0f;}void ReverseAnimation(AnimationId id) {if (auto* a = GetOpacity(id)) a->PlaybackRate *= -1;}// 模擬時間推進void AdvanceTime(Seconds t) {current_time += t;TickAll(current_time);}
private:Seconds current_time = 0;std::unordered_map<AnimationId, AnimationStateProperty<float>> opacityAnimations;// 獲取指定 ID 的動畫狀態AnimationStateProperty<float>* GetOpacity(AnimationId id) {auto it = opacityAnimations.find(id);return it != opacityAnimations.end() ? &it->second : nullptr;}
};
// ---------------- 主函數 -------------------
int main() {AnimationController controller;// 創建一條 opacity 動畫(ID 為 1)AnimationStateProperty<float> opacity;opacity.Id = 1;opacity.StartTime = 0;opacity.Duration = 2.0;opacity.Keyframes.frames = {{0.0, 0.0f},{1.0, 0.5f},{2.0, 1.0f}};controller.AddOpacityAnimation(opacity);// 模擬每 0.5 秒進行一次 tickfor (int i = 0; i <= 5; ++i) {controller.AdvanceTime(0.5);}std::cout << "Pausing...\n";controller.PauseAnimation(1);controller.AdvanceTime(1.0); // 暫停后不應更新std::cout << "Resuming...\n";controller.PlayAnimation(1);controller.AdvanceTime(1.0); // 恢復后繼續更新return 0;
}
總結說明
- 數據導向設計(DoD):以結構體數組組織屬性動畫狀態,提高緩存局部性。
- 模板化設計:通過
TickAnimation<PropType>
支持多個屬性復用邏輯。 - 播放控制清晰:支持暫停、恢復、反向、速率控制、時間跳轉等。
- 可拓展:可以通過添加更多屬性類型來拓展動畫支持。
這部分內容總結了 Data-Oriented Design(數據導向設計,DoD) 的關鍵原則。下面是每一點的詳細中文解釋和分析:
1. 保持數據扁平化(Keep data flat)
目標:最大化 CPU 緩存利用率,減少不必要的內存訪問延遲。
- 扁平化結構(flat struct arrays):
將數據存儲為結構體數組(SoA)而不是對象數組(AoS),例如將動畫的每個字段單獨拆出來存儲在多個數組中,有利于連續訪問、向量化優化。 - 不使用 RTTI(No RTTI):
避免使用dynamic_cast
和typeid
,減少類型檢查開銷。 - 分攤動態分配(Amortized dynamic allocations):
使用std::vector
統一分配內存,避免頻繁的小對象 new/delete,提升性能。 - 適度的只讀冗余(read-only duplication):
對于跨系統需要共享的數據(如 Keyframe 數據),復制副本可能比通過指針引用更快,也更好理解。
2. 基于存在性的分支判斷(Existence-based predication)
目標:減少 if-else
分支,提高分支預測命中率。
- 什么意思?
不使用布爾條件去判斷“是否應該更新”某個動畫,而是將不同狀態的數據放入不同的表里(表結構類似數據庫)。 - 比如:
- 活躍動畫 → 放在
activeAnimations
表里 - 暫停動畫 → 放在
pausedAnimations
表里
- 活躍動畫 → 放在
- 優勢:
- 每次
tick()
時只處理活躍表,無需判斷動畫是否運行。 - 分支預測更友好 → 更少的 pipeline flush,性能更好。
- 每次
3. 基于 ID 的句柄(Id-based handles)
目標:去指針化,允許靈活地管理內存布局。
- 為什么不用指針?
指針耦合數據和內存布局,限制內存遷移與壓縮。 - 使用 ID 替代:
- 每個動畫用一個
AnimationId
(如unsigned int
)表示。 - 外部系統只持有 ID,內部系統通過 ID 查表。
- 好處:
- 內部可以自由移動數據(例如:整理內存、壓縮存儲)
- 更易序列化/跨線程傳遞
- 每個動畫用一個
4. 基于表的輸出(Table-based output)
目標:解耦系統之間的數據流,便于理解和調試。
- 例如:
Tick()
的結果寫入一個OutputTable
,記錄每個動畫更新后的新屬性值(如opacity = 0.5f
)。- 樣式系統、渲染系統從該輸出表讀取并更新。
- 優勢:
- 系統之間通過表交互,而不是直接調用 → 解耦
- 不依賴 DOM、對象樹等復雜結構
- 易于測試、追蹤與回滾
總結一句話:
數據導向設計的核心思想就是:把數據當作第一公民,圍繞數據組織程序邏輯,而不是圍繞對象或繼承結構。
它強調 局部性、扁平化、無分支、ID查表式訪問 和 系統間低耦合高聚合。
如果你需要,我可以:
- 把這套原則應用到一個你自己的項目架構中;
- 幫你重構現有 OOP 風格的動畫系統為 DoD;
- 或者分析你某段性能瓶頸的 C++ 代碼是否適合 DoD 優化。
這部分內容是上關于 面向對象編程(OOP) 與 數據導向設計(DoD) 的系統性對比總結,主要從 性能、可擴展性、可測試性、可修改性 等維度進行分析,并指出各自的優劣。
性能分析(Performance)
項目 | OOP | DoD |
---|---|---|
動畫 tick 平均耗時 | 6.833 ms | 1.116 ms |
結果 | 慢 6 倍 | 快 6.12 倍 |
- 結論:DoD 動畫更新的執行速度比 OOP 快 6 倍以上。
- 原因:
- OOP 有虛函數、多層繼承、分散內存布局、指針跳轉等;
- DoD 是緊湊的線性內存結構、無虛函數、無 RTTI,易于 cache 預取、SIMD、流水線執行。
多線程可擴展性(Scalability)
OOP 的問題:
- 遍歷時容器被修改(iterator invalidation);
- 動畫觸發事件,依賴“委托對象”;
- 動畫更新 DOM 樹結點樣式,涉及全局狀態;
- 解決方法:手動追蹤依賴、加鎖、拆分數據,工作量大,易錯。
DoD 的問題:
- 多線程同時將動畫狀態移到 inactive 表;
- 多線程同時向“修改節點表”中
push_back
。
DoD 的解決方案:
- 每個線程維護自己的局部表(private table):
- 局部動畫輸出;
- 局部 inactive 動畫;
- 使用 fork-join 模式合并;
- 避免共享狀態沖突;
- 易于并行擴展。
可測試性分析(Testability)
OOP:
- 必須 mock:
- 多個類(例如 AnimationEffect、Timeline、Element 等);
- 一個完整的 DOM 結構樹;
- 狀態路徑組合爆炸(太多分支、繼承、虛函數);
- 很難驗證結果是否正確。
DoD:
- 只需要:
- 模擬輸入(動畫定義);
- 模擬結點 ID 列表(不需要完整 DOM);
- Controller 是自包含的;
- 輸出寫入結構化表,易于斷言正確性(斷言 output value 即可)。
可修改性分析(Modifiability)
OOP:
- 修改基類非常困難;
- 一旦寫好類成員后很難移動;
- 對象生命周期難以追蹤,易出錯;
- 雖然“小改快”,但技術債累積嚴重。
DoD:
- 更容易修改 pipeline:
- 修改 input/output 就是前后系統數據變換;
- 局部實現代碼更換容易;
- 可以嘗試新數據結構(SOA/緩存優化);
- 使用句柄(ID)減少生命周期管理問題。
DoD 的缺點
問題 | 解釋 |
---|---|
正確劃分數據很難 | 初期你不了解問題本質,容易亂拆結構 |
Existence-based predication 難用 | 把 bool 拆成表的成本較高,有時不如加個 flag 簡單 |
快速修改困難 | 不能“隨便加個成員”或“繼承一下” |
起步有門檻 | 你可能需要“忘掉 OOP 習慣”重新學習 |
C++ 有時不幫忙 | STL 容器不是為 cache 優化設計,語言默認鼓勵 OOP 寫法 |
OOP 哪些東西該保留?
- 有些場景沒得選,比如:
- 第三方庫;
- DOM IDL 接口;
- 簡單結構 + 簡單方法 是沒問題的;
- Polymorphism 要受控使用:
- 對外 API 接口;
- UI 組件入口;
- 推薦使用 模板 + 編譯期多態 代替虛函數;
- 例如
std::variant
、CRTP、模板 specialization;
- 例如
- 可以根據平臺/配置選擇不同實現(例如 Web/桌面/嵌入式平臺)。
最后建議:語言特性支持
語言建議 | 意義 |
---|---|
允許新內存布局 | 更好支持 SOA / AOS / ECS 等數據結構 |
支持結構化組件布局 | C++ 對組件組合支持不足,DoD 很依賴手工實現 |
引入 ranges | std::ranges 支持 pipeline 式數據流轉換,符合 DoD 精神 |
改進 unordered_map 等容器 | 標準容器分配粒度太細,性能差,DoD 更希望使用 open addressing 結構 |
總結
對比項 | OOP | DoD |
---|---|---|
性能 | 更差,結構復雜,緩存效率低 | 更快,結構緊湊,適合 SIMD + 多線程 |
并發支持 | 依賴鎖,難擴展 | 每線程私有表 + 合并,適合并行 |
測試 | 需要 mock 多對象,狀態難驗證 | 控制器自包含,輸出結構易斷言 |
修改 | 類結構僵硬、難以遷移 | 系統間松耦合、局部改動容易 |
缺點 | 快速開發容易但易累積技術債 | 更難入門,需要重新思考問題劃分 |
如果你想把自己的動畫系統從 OOP 遷移到 DoD,我可以幫你: |
- 拆解現有設計;
- 設計數據表結構;
- 用模板和句柄替代類層級;
- 提供并行或 SIMD 優化思路。