Skip to content

Stage06|JSB & Native Bridge ​

Lesson076|为什么频繁跨 JSB 调用可能很贵 ​

Tags: #Creator2.x #Stage06 #JSB #Performance #BatchingDifficulty: ⭐⭐⭐⭐☆


真实问题:每个对象只同步 0.01 ms,为什么一帧仍然超预算 ​

战斗同步 1000 个对象位置。单次 Native getter 看起来很快,但每帧总成本包括:

text
JSB 进入/返回
参数与返回值转换
C++/平台对象查找
JS 数组/Vec3 创建
1000 次调度与日志
text
0.01 ms × 1000 = 10 ms

跨边界成本要乘调用次数和数据量。

一、本课目标 ​

本课不接受“JSB 一定慢”这种笼统说法,而是拆解一次跨边界调用可能包含的成本,并学习如何用批处理和数据设计减少不必要的往返。


二、一次调用可能做什么 ​

text
JS 函数调用
→ 进入 Binding
→ 参数数量和类型检查
→ JS Value 转 C++ Value
→ 查找 Native 对象
→ 执行 C++ 方法
→ C++ 返回值转 JS Value
→ 异常和状态检查

单次调用可能很快,但每帧、每个对象、每个属性重复时会累积。


三、调用次数比单次成本更重要 ​

text
单次 0.01 ms × 10 次 = 0.1 ms
单次 0.01 ms × 10,000 次 = 100 ms

示例只用于说明数量级,真实成本必须在目标设备和构建模式测量。

常见高频风险:

  • 每帧逐个读取 Native 对象属性。
  • 大列表逐项跨边界更新。
  • 大量小粒子分别调用平台接口。
  • JS/C++ 之间传递深层对象和数组。

四、属性读取也可能是跨边界调用 ​

ts
for (const item of nativeItems) {
    total += item.x;
    total += item.y;
}

如果 x、y 是绑定属性,每次 getter 都可能进入 Binding。改进方向可能是批量获取:

ts
const positions = nativeManager.getAllPositions();

但批量返回也有复制成本,需要在“调用次数”和“数据量”之间平衡。


五、批处理的思想 ​

多次小调用 ​

text
JS → C++ setX
JS → C++ setY
JS → C++ setScale
JS → C++ setColor

一次结构化调用 ​

text
JS → C++ applyState(state)

批处理可以减少边界往返,但不应把整个场景状态每帧序列化成巨大对象。合适粒度取决于数据大小、变化频率和 Native 接口设计。


六、复制和转换成本 ​

下面的数据跨边界时可能发生大量工作:

text
大数组
深层对象
长字符串
二进制数据
大量回调

优化方向:

  • 传递紧凑数据。
  • 减少深层对象遍历。
  • 使用适合的二进制缓冲方式。
  • 避免相同数据重复转换。
  • 明确所有权和是否需要复制。

七、缓存也有风险 ​

为了减少 getter 调用,可以在 JS 缓存 Native 状态:

ts
this.cachedValue = nativeObject.getValue();

但要解决:

text
Native 状态变化后谁更新缓存?
缓存过期如何发现?
对象销毁后缓存是否仍有效?

性能优化不能破坏数据一致性。


八、案例:每帧同步 1000 个对象 ​

错误模型:

text
for each object
    get native x
    get native y
    set JS node x
    set JS node y

可能产生数千次边界往返。改进方向:

text
Native 一次计算所有状态
→ 批量返回紧凑结果
→ JS 只更新真正变化或可见对象

也可以重新判断:这些对象是否真的需要同时存在于 JS 和 C++ 两套状态中。


九、如何证明是 JSB 成本 ​

text
记录跨边界调用次数
→ 在目标设备采样调用总耗时
→ 构造批量接口对照
→ 保持业务数据量一致
→ 比较 Frame Time 和结果正确性

不要只看到堆栈包含 binding 就断言根因是 JSB。


十、常见误区 ​

误区一:所有 JSB 调用都应该删除 ​

原生能力必须通过边界访问,关键是频率、数据量和接口粒度。

误区二:一次传入巨大对象一定比多次小调用快 ​

巨大对象转换和复制也可能很贵。

误区三:缓存 Native 值永远安全 ​

缓存会带来一致性和生命周期问题。

误区四:编辑器预览能准确测量 JSB ​

Web 预览可能根本不走相同的 Native Binding 路径。


十一、练习与答案 ​

  1. 一次 JSB 调用可能包含哪些成本?
  2. 为什么高频 getter 也可能成为问题?
  3. 批处理有什么收益和代价?
  4. 如何验证 JSB 是真实性能瓶颈?

答案:

  1. 类型检查、数据转换、对象映射、C++ 调用、返回值和异常处理。
  2. 每次 getter 都可能跨边界,调用次数会累积。
  3. 减少往返,但可能增加数据复制、延迟和接口复杂度。
  4. 在 Native 目标设备统计次数和总耗时,使用批量接口做单变量对照。

Creator 2.4.x 实验:逐个调用与批量调用 ​

JsbCostProbe.ts ​

ts
const { ccclass } = cc._decorator;

@ccclass
export default class JsbCostProbe extends cc.Component {
    private values: number[] = [];
    private nativeCall: (value: number) => number = null;

    start() {
        for (let i = 0; i < 1000; i++) {
            this.values.push(i);
        }
    }

    runPerItem() {
        const begin = Date.now();
        let result = 0;
        for (const value of this.values) {
            result += this.readOne(value);
        }
        cc.log('[jsb-cost] per item ms=', Date.now() - begin);
        return result;
    }

    runBatch() {
        const begin = Date.now();
        const result = this.readBatch(this.values);
        cc.log('[jsb-cost] batch ms=', Date.now() - begin);
        return result;
    }

    private readOne(value: number) {
        return value;
    }

    private readBatch(values: number[]) {
        let result = 0;
        for (const value of values) {
            result += value;
        }
        return result;
    }
}

示例先用脚本模拟调用形状;要测真实 JSB,替换为项目已存在且安全的 Native API,并在 Web/Native 分开记录。不要把脚本循环结果冒充 JSB 结果。

实验步骤 ​

  1. 对比 1、100、1000、10000 个值。
  2. 对比逐个 getter、一次批量 API(若项目已有)。
  3. 分开测纯 JS、Web API 和 Native 包。
  4. 关闭日志,避免日志成为瓶颈。
  5. 记录数据复制与结果对象创建。

批量 API 为什么常有效 ​

text
差的接口:JS → Native → JS,重复 N 次
好的接口:JS → Native 一次,返回一批结果

批量也不是无限增大:大数组复制、峰值内存、延迟和部分失败需要处理。最佳批大小由数据量与平台测量。

读属性也可能跨边界 ​

代码看似只是:

ts
const x = node.x;

实际是否访问 Native、是否缓存、是否同步 Transform,取决于对象与版本。不要凭语法猜成本,使用采样与源码/日志验证。

真实项目故障:每帧读取 Native 节点矩阵 ​

UI 需要 1000 个 Native Node 世界位置。逐节点调用 getter 后在 JS 做排序,CPU 长帧明显。

优化:

  • 减少需要跨边界的数据;
  • 在 Native 侧批量计算/筛选;
  • 降低采样频率;
  • 只同步可见/变化对象;
  • 用事件推送变化而不是每帧轮询;
  • 验证排序结果与坐标时序。

证明是 JSB 成本的对照 ​

text
版本 A:纯 JS 数据结构
版本 B:相同数量模拟对象
版本 C:真实 Native API
版本 D:批量 Native API

若 C 明显高于 A/B 且随调用次数线性上升,JSB/Native 进入边界是嫌疑;D 改善则进一步支持批量假设。

版本边界与练习答案 ​

JSB 成本受 JavaScript Engine、绑定生成、Native ABI、平台和数据类型影响,不能使用固定“每次多少微秒”。

  1. 单次很快为什么总成本仍高? 调用次数、参数转换和返回对象分配累加。
  2. 批量 API 的代价? 大参数复制、延迟、内存峰值和错误粒度。
  3. 如何避免误把纯 JS 成本当 JSB? 做纯 JS、模拟和真实 Native 对照。
  4. 为什么事件推送常优于轮询? 变化发生时才跨边界,降低无效读取。

十二、本课总结 ​

text
JSB 成本取决于调用次数、数据量、转换复杂度和目标设备。
优化重点是减少不必要往返,选择合适的数据粒度,并保持状态一致。

十三、下一课预告 ​

text
Lesson077|GC、引用计数与跨语言对象生命周期