当前位置: 首页 > news >正文

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.lryc.cn/news/580138.html

相关文章:

  • ABP VNext + RediSearch:微服务级全文检索
  • PyCharm 安装使用教程
  • Rust异步爬虫实现与优化
  • 全星 QMS:制造业全面质量管理的数字化全能平台
  • 鸿蒙系统(HarmonyOS)应用开发之手势锁屏密码锁(PatternLock)
  • Jenkins-Publish HTML reports插件
  • 接口测试之postman
  • ZigBee通信技术全解析:从协议栈到底层实现,全方位解读物联网核心无线技术
  • 区块链技术核心组件及应用架构的全面解析
  • 7.4_面试_JAVA_
  • 【PyTorch】PyTorch预训练模型缓存位置迁移,也可拓展应用于其他文件的迁移
  • 基于PHP+MySQL实现(Web)英语学习与测试平台
  • 408第三季part2 - 计算机网络 - 计算机网络基本概念
  • 金融平衡术:创新与合规的突围之路
  • Spark从入门到实战:安装与使用全攻略
  • 使用 DigitalPlat 免费搭配 Cloudflare Tunnel 实现飞牛系统、服务及 SSH 内网穿透教程
  • Java SE--方法的使用
  • Kotlin中优雅的一行行读取文本文件
  • 缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级
  • 【笔记】PyCharm 2025.2 EAP 创建 Poetry 和 Hatch 环境的踩坑实录与反馈
  • 三体融合实战:Django+讯飞星火+Colossal-AI的企业级AI系统架构
  • Android WebView 性能优化指南
  • 《Java修仙传:从凡胎到码帝》第三章:缩进之劫与函数峰试炼
  • React Ref使用
  • React中的useState 和useEffect
  • 指环王英文版魔戒再现 Part 1 Chapter 01
  • 力扣 hot100 Day34
  • [Linux]内核态与用户态详解
  • java web5(黑马)
  • Vue内置指令