CppCon 2018 學習:OOP is dead, long live Data-oriented design

探討了面向對象編程(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,比如 AnimationKeyframeEffect 等類,來操作和控制動畫。
    這說明 CSS 動畫背后有一套對象模型和邏輯支持,而不是單純的靜態樣式聲明。

總結

  • CSS 動畫表面看起來簡單,聲明式,易用;
  • 但實現上涉及多種不同數據類型的插值算法;
  • 還有配套的 DOM 動畫 API,支持更靈活的控制和管理動畫。

Chromium(Google 瀏覽器內核)中 Blink 動畫系統的 OOP 設計,幫你總結和理解一下:

Chromium Blink 動畫系統的 OOP 設計特點

  1. 兩個動畫系統
    Chromium 實際上有兩個動畫系統,這里關注的是 Blink 動畫系統。
  2. 經典面向對象設計
    • 代碼大量用到繼承和接口(多重繼承),符合傳統 OOP 風格。
    • 類名上體現出 final(表示類不可繼承),說明設計時對繼承做了限制。
    • 繼承鏈長,接口多,職責分散在不同的類和接口上。
  3. 符合 HTML5 標準和 IDL
    Blink 動畫設計嚴格遵循 HTML5 的動畫規范和接口定義語言(IDL),以保證標準兼容。
  4. 動畫對象設計
    • 運行中的動畫被抽象成獨立的對象。
    • 每個動畫對象負責自身的狀態和行為,利用面向對象的封裝和多態。
  5. 類聲明示例
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::ServiceAnimationsAnimation::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 &current_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 對象持有它的 AnimationEffectReadOnlyDocumentTimeline 的智能指針。
  • 這樣避免了手動內存管理,防止懸空指針和內存泄漏問題。
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為快
};

代碼分析

  1. 結構體作用
    這是一個用于保存單個動畫狀態的扁平化數據結構,所有動畫相關的時間點、播放控制和狀態數據都集中在這里。避免分散在多個類或對象中,利于高效批量處理。
  2. 時間點字段
    • StartTimePauseTime 精確記錄動畫的起始和暫停時間,便于計算當前播放進度。
    • DurationDelay 明確動畫持續時間和延遲時間,有助于計算動畫何時真正開始和結束。
    • ScheduledPauseTime 是可選的,支持計劃中的暫停操作,比如動畫運行到某時間自動暫停。
  3. 播放行為控制
    • Iterations 支持循環播放,能控制動畫重復次數或無限循環。
    • FillMode 控制動畫結束后元素是否保持動畫結束狀態,常見的如“forwards”、“backwards”等。
    • Direction 支持多種播放方向,方便做來回動畫或交替播放。
    • Timing 是緩動函數,決定動畫進度變化曲線,影響動畫的視覺節奏。
  4. 播放狀態和進度
    • PlayState 顯示當前動畫是播放中還是暫停或停止。
    • IterationsPassed 精確表示動畫已播放的進度(可細分到小數),便于精細控制動畫播放。
    • PlaybackRate 允許加速或減速播放,支持更靈活的動畫效果。
  5. 設計理念
    • 通過扁平結構避免復雜指針引用和對象嵌套,提升數據訪問的局部性(cache locality)。
    • 結構體中只包含數據,沒有行為,符合數據導向設計思想,方便用數據驅動的批量動畫更新和調度。

額外說明

  • mono_clock 是模擬的單調時鐘類型,保證時間值不會倒退,適合動畫計時。
  • 具體枚舉類型(AnimationFillMode::TypeAnimationDirection::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)的判斷。
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_casttypeid,減少類型檢查開銷。
  • 分攤動態分配(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)

項目OOPDoD
動畫 tick 平均耗時6.833 ms1.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 很依賴手工實現
引入 rangesstd::ranges 支持 pipeline 式數據流轉換,符合 DoD 精神
改進 unordered_map 等容器標準容器分配粒度太細,性能差,DoD 更希望使用 open addressing 結構

總結

對比項OOPDoD
性能更差,結構復雜,緩存效率低更快,結構緊湊,適合 SIMD + 多線程
并發支持依賴鎖,難擴展每線程私有表 + 合并,適合并行
測試需要 mock 多對象,狀態難驗證控制器自包含,輸出結構易斷言
修改類結構僵硬、難以遷移系統間松耦合、局部改動容易
缺點快速開發容易但易累積技術債更難入門,需要重新思考問題劃分
如果你想把自己的動畫系統從 OOP 遷移到 DoD,我可以幫你:
  • 拆解現有設計;
  • 設計數據表結構;
  • 用模板和句柄替代類層級;
  • 提供并行或 SIMD 優化思路。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/87660.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/87660.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/87660.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

科學的第五范式:人工智能如何重塑發現之疆

在人類探索未知的壯闊史詩中&#xff0c;科學方法的演進如同照亮迷霧的燈塔。從基于經驗的第一范式&#xff08;描述自然現象&#xff09;&#xff0c;到以理論推演為核心的第二范式&#xff08;牛頓定律、麥克斯韋方程&#xff09;&#xff0c;再到以計算機模擬為標志的第三范…

tmux 左下角會話名顯示不全的解決方法

在 tmux 中顯示完整的會話名 有時候我們要在服務器上長時間跑某個任務&#xff0c;但不可能時時刻刻保持終端模擬器開啟&#xff0c;這時候就需要用到 tmux &#xff0c;可以在關閉會話的同時讓任務繼續在后臺跑&#xff0c;后續還可以連回來。但在 tmux 會話中&#xff0c;左…

【期末分布式】分布式的期末考試資料大題整理

&#x1f9f8;安清h&#xff1a;個人主頁 &#x1f3a5;個人專欄&#xff1a;【Spring篇】【計算機網絡】【Mybatis篇】 &#x1f3af;大題 ?一.Nacos的服務注冊與發現 &#x1f6a6;1.怎么來進行服務的注冊與發現的這樣的一個流程&#xff0c;描述一下。 &#x1f383;描述…

Android手機無網離線使用FunASR識別麥克風語音內容

手機斷網離線使用FunASR識別麥克風語音內容 --本地AI電話機器人 上一篇&#xff1a;阿里FunASR本地斷網離線識別模型簡析 下一篇&#xff1a;手機無網離線使用FunASR識別手機歷史通話錄音 一、前言 繼上一篇《阿里FunASR本地斷網離線識別模型簡析》和前面幾篇ASR相關理論的…

Stable Diffusion 項目實戰落地:從0到1 掌握ControlNet 第五篇 線稿到高清修復:一步步教你用AI做出完美IP形象

大家好!上一篇,我們一起玩轉了字體風格變換 ,讓文字根據提示詞進行自如變換,個性十足又充滿創意! 如果你錯過了那篇文章,別擔心,趕緊點這里補課:Stable Diffusion 項目實戰落地:從0到1 掌握ControlNet 第四篇 風格化字體大揭秘:從線稿到涂鴉,ControlNet讓文字煥發新生…

Java網絡編程:TCP/UDP套接字通信詳解

TCP客戶端套接字創建與使用 Socket類基礎概念 Socket類的對象代表TCP客戶端套接字&#xff0c;用于與TCP服務器套接字進行通信。與服務器端通過accept()方法獲取Socket對象不同&#xff0c;客戶端需要主動執行三個關鍵步驟&#xff1a;創建套接字、綁定地址和建立連接。 客戶端…

VMware vSphere 9與ESXi 9正式發布:云原生與AI驅動的虛擬化平臺革新

2025年6月18日&#xff0c;VMware正式推出其旗艦虛擬化平臺vSphere 9及配套的ESXi 9操作系統&#xff0c;標志著企業級虛擬化技術邁入以云原生、人工智能&#xff08;AI&#xff09;和硬件加速為核心的新紀元。此次更新不僅在功能層面實現突破&#xff0c;更通過授權模式革新為…

汽車功能安全概念階段開發【相關項定義HARA】2

文章目錄 1 淺談概念階段開發2 功能安全概念階段開發2.1 相關項定義2.2 危害分析與風險評估&#xff08;HARA-Hazard Analysis and Risk Assessment&#xff09; 3 關鍵輸出與對后續階段的影響4 總結 1 淺談概念階段開發 概念階段開發是整個研發流程的起點和基石。它發生在任何…

WPF中依賴屬性和附加屬性

依賴屬性&#xff08;DependencyProperty&#xff09; 依賴屬性是WPF中的一種特殊屬性&#xff0c;它的實現依賴于DependencyObject類提供的基礎設施。與普通的.NET屬性不同&#xff0c;依賴屬性的值可以通過多種方式確定&#xff0c;包括繼承、樣式、數據綁定和動畫等。 主要特…

Docker 中如何實現鏡像的推送和拉取

在 Docker 中&#xff0c;鏡像的推送&#xff08;push&#xff09;和拉取&#xff08;pull&#xff09;是通過與**Docker 鏡像倉庫&#xff08;Registry&#xff09;**交互完成的。默認倉庫是 Docker Hub&#xff0c;但你也可以使用私有倉庫&#xff08;Harbor、Nexus、AWS ECR…

[C#] WPF - 自定義樣式(Slider篇)

一、定義樣式 在App.xaml里面定義樣式&#xff1a; <Applicationx:Class"WpfApp.StudySlider.App"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"xmlns:local&q…

eBPF 實戰指南:精準定位 TCP 重傳,洞察網絡瓶頸真相

更多云服務器知識&#xff0c;盡在hostol.com 你有沒有遇到過這種情況&#xff1f;網站訪問卡頓&#xff0c;接口響應慢得像蝸牛爬。你 ping 服務器沒丟包&#xff0c;CPU 內存也沒打滿&#xff0c;日志也沒報錯&#xff0c;結果就是不知道哪兒出的問題。 你用抓包分析&#x…

在 Ubuntu 系統上安裝 Docker 環境

在當今的開發環境中&#xff0c;Docker 已經成為容器化技術的主流選擇。它可以幫助開發者輕松地創建、部署和運行應用程序。本文將詳細介紹如何在 Ubuntu 系統上安裝 Docker 和 Docker Compose&#xff0c;并解決在安裝過程中可能遇到的一些常見問題。 一、安裝 Docker 1.卸載舊…

【Qt】QxORM無法刪除和更改主鍵值為0的行,否則報錯:invalid primary key

1、問題描述 使用 QxORM 刪除或者更改數據庫時,當主鍵值為 0 時,報錯: [QxOrm] invalid primary key2、原因分析 2.1 源碼分析 查找打印錯誤提示的代碼: #define QX_DAO_ERR_INVALID_PRIMARY_KEY "[QxOrm] invalid primary key" QSqlError IxDao_Help…

數學建模_線性規劃

問題背景模型介紹matlab求解 示例 問題背景 模型介紹 matlab求解 max問題轉化為min問題 > > >號轉化為 < < <號 示例 看到多個線性規劃目標 2個目標函數變成1個目標函數 后面省略

51單片機制作萬年歷

硬件設計 主控芯片&#xff1a;一般選用AT89C52單片機&#xff0c;它與MCS - 51單片機產品兼容&#xff0c;有8K字節在系統可編程Flash存儲器、32個可編程I/O口線、三個16位定時器 / 計數器等。時鐘芯片&#xff1a;常用DS1302時鐘芯片&#xff0c;能提供實時時鐘 / 日歷、定時…

Oracle CTE遞歸實現PCB行業的疊層關系

1、需求背景&#xff0c;出貨報告要實現疊板假層的處理&#xff0c;需求如下 表ID,layer,MEDIUM數據如下 第一種情況&#xff0c;layer有K的 IDlayerMEDIUM1L1-L2302L2-L3403L3-K1204K1-L4105L4-L5206L5-L6307L7-K2108K2-L8119L8-L91010L9-L1030 實現layer有K1的&#xff0c…

Kubernetes 服務發布基礎學習

一、Service 概述&#xff08;一&#xff09;Service 的定義Service 是 Kubernetes 中的一種抽象概念&#xff0c;用于定義一組 Pod 以及訪問這組 Pod 的策略。其核心作用是將一組 Pod 封裝為一個虛擬服務&#xff0c;并為客戶端提供統一的入口&#xff0c;從而實現服務的負載均…

【零基礎學AI】第21講:TensorFlow基礎 - 神經網絡搭建入門

本節課你將學到理解什么是TensorFlow&#xff0c;為什么要用它 掌握TensorFlow安裝和基本操作 學會搭建第一個神經網絡 完成手寫數字識別項目 開始之前 環境要求 Python 3.8至少4GB內存網絡連接&#xff08;用于下載數據集&#xff09; 前置知識 第1-8講&#xff1a;Python基礎…

STM32 串口USART通訊驅動

前言 本篇文章對串口Usart進行講解&#xff0c;為后面的esp8266和語音模塊控制打好基礎。 1.串口USART USART&#xff08;Universal Synchronous/Asynchronous Receiver/Transmitter&#xff0c;通用同步 / 異步收發器&#xff09; 是一種常見的串行通信接口&#xff0c;廣泛應…