Skip to content

路线 A|Cocos 引擎、渲染与性能 ​

A1|Engine Architecture:从公开 API 追到引擎实现 ​

适用基础: Stage01~Stage07 目标: 面对“Creator 为什么这样运行”的问题,能够锁定版本、画出调用链、解释对象状态,并用公开语义与源码实现交叉理解。


一、本阶段要解决的真实问题 ​

text
调用 loadScene 后旧节点为什么还在?
同一个 API 在 Web 和 Native 为什么表现不同?
Prefab 实例的属性引用从哪里恢复?
事件明明触发了,为什么回调拿到的对象已经失效?

这些问题不能靠记 API 解决。通用排查链是:

text
业务入口
→ Creator 公共包装
→ JavaScript Engine / JSB
→ Cocos2d-x 对象
→ Scheduler / Scene / Renderer
→ 平台实现

二、对象体系与所有权 ​

Node 是层级和 Transform 的运行时对象,Component 是挂载在 Node 上的行为对象,CCObject 提供引用、销毁和序列化相关基础能力。业务代码应区分:

text
编辑器资源:Scene/Prefab/Texture 文件
运行时对象:Scene/Node/Component
观察引用:脚本属性、事件回调、缓存索引
所有者:节点树、资源缓存、常驻服务或 Native wrapper

对象状态的知识边界 ​

Node 脱离父节点只改变层级关系,不自动销毁;active=false 让节点从层级调度和渲染语义中失活,但对象仍可能被缓存;destroy() 请求结束对象生命周期,真正清理通常在引擎安全点完成;isValid 反映的是销毁有效性,不是“当前是否显示”。

因此排查一个“对象还在不在”的问题,至少要同时问四件事:对象是否仍被 owner 持有、是否仍在 Scene 树、是否满足 active/enabled 调度条件、是否已经进入销毁流程。把这四个维度混成一个布尔值,是场景切换和对象池 bug 的常见根源。

三、Director 与调度链 ​

text
平台帧回调
→ cc.game / mainLoop
→ Director
→ Scheduler
→ ComponentScheduler
→ update / lateUpdate
→ Transform dirty 传播
→ Renderer 提交

源码阅读顺序:先看公开入口和字段,再看一次调用链,最后看队列如何增删。不要从一个私有函数的名字推断完整架构。

Director 为什么不是“超级管理器” ​

Director 保存当前 Scene、协调场景切换并参与一帧推进,但它不应该拥有所有业务状态。调度职责通过 Scheduler 和 ComponentScheduler 分担:前者推进时间任务,后者根据组件和节点状态维护生命周期回调。这样做的价值是让“当前场景变化”和“任务是否继续”可以分别表达。

一帧中稳定的职责链可以抽象为:计算时间输入、推进调度、执行组件逻辑、整理 Transform/渲染状态、提交画面。具体内部函数和队列插入点必须锁定 Creator 2.4.x 小版本源码理解,不能把抽象链当成逐行源码。

四、Scene、Prefab 与反序列化 ​

text
资源路径/UUID
→ AssetDB/加载器
→ 序列化数据
→ Node/Component 对象图
→ 引用恢复
→ 激活与生命周期

不要手工改 .scene、.prefab 或 .meta。用编辑器创建资源,用运行时探针验证 onLoad、onEnable、start 的时间点。场景切换问题应同时检查资源依赖和残留事件监听。

五、EventTarget 与输入事件 ​

捕获、目标和冒泡是事件传播的三个观察点。命中不等于回调一定执行,还受节点激活、组件 enabled、遮挡节点和事件监听生命周期影响。

事件系统的关键不是“回调有没有注册”,而是事件从哪里产生、如何命中、沿哪条树传播以及监听器是否仍然属于有效页面。捕获阶段适合父层做拦截,目标阶段处理自身行为,冒泡阶段适合父层聚合;遮挡、优先级、activeInHierarchy 和监听 owner 任一项改变,都可能让同一点击得到不同结果。页面组件应在 onEnable 注册、在 onDisable 取消,避免常驻对象持有旧页面。

六、Loader、依赖和缓存 ​

区分:

text
资源请求完成 ≠ 依赖全部释放
destroy Node ≠ 资源引用归零
缓存命中 ≠ 业务对象仍然有效

资源系统至少包含请求、依赖解析、缓存、反序列化、实例化和释放几个语义层。请求命中缓存只能说明资源数据可复用,不能说明由它创建的 Node 仍有效;销毁 Node 也不会自动让其他 SpriteFrame、Material、Bundle 或闭包引用归零。判断泄漏时要区分对象仍存活、资源仍被缓存和平台 allocator 尚未把内存还给系统。

七、JSB 与平台边界 ​

追踪一个 Native 能力时,固定 Creator 版本、构建模式和平台,依次搜索:

text
TypeScript API
→ JS 包装
→ Binding 注册
→ 参数转换
→ C++ 对象/函数
→ 平台 API

后台线程不能直接调用脚本引擎或改 Scene。回调必须回到规定线程,并检查页面 session 和 Native 对象仍有效。

八、综合案例:Web 正常、Native 崩溃 ​

排查顺序:参数类型和空值 → Binding 对象映射 → callback 生命周期 → 线程 → 文件/权限 → 图形或平台 API → 符号化堆栈。先用最小 Native 能力验证边界,再回到完整业务,不要直接修改生成目录。

九、阶段知识总结:ApiTrace 的分层方法 ​

选择 cc.director.loadScene 或资源加载 API,按以下层次阅读:

  1. 公共 API 的输入、返回值和完成语义。
  2. Creator 包装层如何保存回调、对象引用和错误。
  3. JSB/Binding 如何转换参数、定位 C++ 对象并切线程。
  4. C++ 对象由谁拥有、在哪个阶段释放。
  5. 平台 API 的权限、线程、路径和异步限制。
  6. 回调返回时页面/session 是否仍然有效。

学习本阶段真正要掌握的是把“公开语义、对象所有权、线程边界、版本实现”分开解释,而不是背一张固定调用图。日志和最小案例只在结论存在版本差异时用于辅助验证。

十、常见错误方案 ​

text
把业务状态全部塞进 Director:造成全局耦合
把常驻节点当万能单例:场景引用无法清理
用 sleep 等待场景加载:时序不可证明
猜私有函数名称:升级后结论失效
用裸 this 保存异步回调:页面退出后 use-after-free

十一、练习与答案 ​

1. 如何判断问题属于资源层还是生命周期层? ​

先确认资源回调是否成功,再确认运行时对象是否创建和激活,最后检查 owner、active/enabled 和 session。不要看到 null 就直接重试加载。

2. 为什么必须锁定小版本? ​

公开语义可能稳定,但调度顺序、事件名、绑定实现和缓存清理会随小版本与平台改变;没有版本就无法复现源码证据。

3. 怎样证明一个 Native 崩溃是回调生命周期问题? ​

让页面立即退出并使 session 失效,记录 callback 创建、取消、投递和释放;若关闭路径稳定复现且正确拒绝过期结果,才能把假设推进为证据。

十二、阶段输出 ​

完成 A1 后应能够独立产出版本表、调用链图、对象所有权图和故障矩阵。这套分析框架会贯穿 A2、A3 和 A4。

下一阶段进入 GPU 和 Renderer,不再停留在“DrawCall 多所以卡”的结论,而是追踪一帧如何到达 GPU。