把 JSI 用到工程里,首先要判断一件事该不该同步跨过去。接口能同步,不代表业务就应该同步。一次调用的成本大致由三段组成:从 JavaScript 进入 C++ 的开销、原生侧真正做的工作、结果写回 JavaScript 的开销。JSI 压低的是前后两段。中间那段如果本身要几十毫秒,换不换接口都不会让界面变顺。因此落地时先把调用分成两类:即时查询和耗时任务。即时查询适合 HostObject 或 TurboModule 的同步方法,例如读取已经驻留内存的配置、取一个开关状态、计算一个不依赖磁盘的结果。耗时任务保持异步,在后台线程完成后,再用回调或 Promise 把结果送回。

  HostObject 适合“对象在 JavaScript 里,数据在原生侧”的形状。脚本拿到的是一个看起来普通的对象,读属性时才触发原生取值。这样做有两个好处。一是避免启动时序列化整份数据。二是原生数据变化后,下一次读取就能看到新值,不必再推一次全量更新。用的时候要克制属性数量和取值成本。如果某个 getter 里隐含了锁等待或跨进程读取,调用方会以为自己只是在读字段,实际却堵住了 JavaScript 线程。取值函数应当短小,复杂逻辑留在明确命名的方法里,让调用代价在接口上可见。

  TurboModule 适合一组稳定的方法,而不是零散挂到全局对象上的函数。接口文件是契约:方法名、参数类型、返回值是否异步,都写在这里。代码生成保证 JavaScript、C++ 和平台实现三者一致,手写绑定里常见的参数错位会在生成阶段暴露。模块按需加载也来自这套契约。运行时只有在第一次访问该模块时才执行安装,把初始化从应用启动挪到功能真正发生的时刻。对很少被打开的页面,这个差别会直接体现在启动耗时上。

  跨语言的数据不要贪多。JSI 虽然不必把对象先变成 JSON 字符串,但大型数组、深层嵌套对象在边界上仍要逐项转换。频繁来回传递几千个元素,开销会重新堆积起来。更稳妥的做法是把大数据留在原生侧,JavaScript 只拿句柄,需要展示的那一页再取一小段。图片缓冲、音频采样、长列表的原始记录,都属于这一类。句柄可以是 HostObject,也可以是一个只有标识和少量方法的轻对象。

  线程是第二件要写进设计的事。同步方法默认跟随调用它的那条 JavaScript 线程。原生实现里如果要更新界面,必须切回平台的 UI 线程,不能在 C++ 方法里直接改控件。反过来,后台线程也不能随意调用只在 JavaScript 线程上有效的运行时接口。需要从原生主动通知脚本时,先把任务派发到 JavaScript 线程,再调用事先持有的函数对象。这个函数对象要用运行时的持有类型保管,否则脚本侧一旦释放,原生侧再调用就是非法访问。模块卸载、页面销毁、引擎重建,都要走同一套释放路径。

  错误处理要让调用方分得清失败原因。参数不合法、设备不可用、权限不足,应当变成 JavaScript 里可捕获的异常或明确的错误对象,而不是让 C++ 异常穿透运行时。同步方法尤其如此,因为调用栈同时跨着两门语言,定位时如果没有统一的错误类型,日志里只会剩下一次崩溃地址。

  调试阶段建议把边界调用单独打点:方法名、耗时、是否发生线程切换。JSI 的问题很少表现为“调不通”,更多表现为“能调但偶发卡一下”。打点能把卡顿定位到某一个 getter 或某一次锁竞争,而不是笼统地归咎于新架构。也要保留一条回退:个别模块在旧设备上若遇到引擎差异,可以暂时走异步实现,接口形状保持不变,这样业务侧不用跟着改。

  把这些约束收拢之后,JSI 的用法就清楚了。用它缩短确定、轻量、需要立即返回的调用;把重活、大数据和线程切换留在接口边界之外;用代码生成固定契约,用持有类型管住生命周期。这样写出的模块,性能收益来自少做的那些事,调用路径也保持可见。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐