文章主旨
Part 5 让 JavaScript 可以持有 HostObject,但真正麻烦的问题从这里才开始:
JS 和 C++ 之间流动的那些字节,到底归谁所有?
这篇文章回答的是 JSI 最核心、也最容易出事故的问题:
- JS 传给 native 的数据是复制还是共享?
- native 返回给 JS 的对象什么时候会被释放?
shared_ptr在 JS GC 参与后到底扮演什么角色?- 为什么字符串通常是 copy,而
ArrayBuffer才有机会 zero-copy?
原文把这篇定位为"两个堆之间的边界地图",这个说法非常准确。
两个堆、两套规则、一个边界
这篇首先把问题讲得很直白:
- JavaScript 有自己的 GC heap;
- C++ 有自己的 native heap;
- 它们不是同一套内存系统;
- 它们也不共享同一套回收协议。
所以在 JSI 中最危险的思维错误是:
把 JS 和 C++ 想成同一个内存宇宙。
它们不是。
更准确的理解是:
- JS GC 只理解 JS 对象图;
- C++ 只理解自己显式管理的对象生命周期;
- JSI 只是让你在边界上建立受控连接。
这也是为什么原文用"两个图书管理员共管一本书"来类比所有权问题。
shared_ptr 是连接两个世界的桥,但它不是魔法
Part 5 里 shared_ptr 已经出现过,这篇把它的作用讲得更完整:
- C++ 侧持有一份共享所有权;
- JS runtime 包装出来的 HostObject 也间接持有一份;
- 最后一个引用消失时,底层对象析构。
这意味着 shared_ptr 的价值不只是"方便不手写 delete",而是:
它把 JS 对 native 对象的持有关系,纳入 C++ 可推理的生命周期模型。
但这不代表问题自动消失。因为:
shared_ptr只能管理它知道的对象;- 它不自动托管你从对象里再解出来的裸指针;
- 它不自动帮你处理跨线程 use-after-free;
- 它也不替你决定数据该 copy 还是 share。
所以它只是桥,不是保险箱。
一个非常关键的问题:返回的方法怎么安全持有对象
这篇专门展开了 Part 5 留下来的一个坑:在 HostObject 的 get() 里返回方法时,如何保证方法闭包里的对象不悬空。
如果闭包只抓裸 this,那么:
- JS 可能长期持有这个方法;
- HostObject 本体却可能先被释放;
- 之后调用方法时就会命中悬空地址。
原文把这称为 dangling method problem。
更安全的思路是:
- 让对象由
shared_ptr托管; - 用
shared_from_this()获取安全共享所有权; - 或在需要避免循环持有时配合
weak_ptr。
这背后的本质是:
从 HostObject 返回出去的方法,本身也会把对象生命周期再拉长一层。
很多 JSI 崩溃都不是对象"没创建",而是对象"死得比你以为的早"。
字符串:默认就是复制,不要幻想零拷贝
这篇一个非常值得记住的结论是:
JSI 字符串基本上总是 copy。
无论是:
- JS string 转 C++
std::string - C++ UTF-8 string 转 JS string
都会发生拷贝。
这件事的工程意义很大:
- 小字符串问题不大;
- 高频大文本就会贵;
- 如果你处理的是二进制或大块数据,字符串不是正确通道。
很多人第一次做 JSI 容易把一切都往 string 上塞,这篇明确告诉你:那通常不是高性能路径。
ArrayBuffer 才是 JSI 里真正有价值的零拷贝能力
与 string 相对,原文强调 ArrayBuffer 是 JS 世界中少数适合和 native 共享底层字节的类型。
这意味着:
- JS 可以把二进制数据给 native;
- native 可以读取底层 buffer;
- native 也可以构造
ArrayBuffer再交给 JS; - 在某些设计下,双方可以看的是同一片字节,而不是两份副本。
这也是为什么音频、图像、视频帧、模型输入输出这类场景,会天然偏向 ArrayBuffer 而不是 string / JSON。
但原文同时提醒:零拷贝不是白送的。
只要你选择共享,就必须把所有权问题想清楚:
- 谁分配的?
- 谁释放的?
- 共享期间谁可以写?
- 跨线程还能不能安全读?
零拷贝换来的不是"只有性能收益",而是"更高的所有权复杂度"。
这篇最值钱的一部分:所有权矩阵
原文其实在反复建立一个判断框架:数据跨边界时,通常落在四种模式之一:
- JS 拷贝给 C++
- C++ 拷贝给 JS
- JS 与 C++ 共享同一份底层数据
- 某一方只做短期借用,不保留长期所有权
你在设计 JSI API 时,真正该先问的不是"能不能实现",而是:
- 这次边界跨越属于哪一类?
- 生命周期是否超出当前调用栈?
- 是否会跨线程继续使用?
这个判断比具体 API 名字更重要。
三类典型内存 bug
原文把跨边界内存问题浓缩成三类 bug,这个总结非常好:
1. Use-after-free
对象已经被释放,但另一边还在继续读写。
最常见场景:
- 背景线程还拿着裸指针;
- HostObject 已被 GC 释放;
- 闭包里仍然访问旧内存。
2. Memory leak
对象本来应该释放,但由于循环持有、遗漏释放或长期共享引用,导致一直活着。
典型场景:
shared_ptr环;- JS 长期引用一个原本应当短生命周期的对象;
- native 缓存没有及时清空。
3. Raw allocation 漏洞
绕过 RAII / 智能指针,手写原始分配后忘记回收。
这是最传统、也最没必要的错误。原文的立场很明确:现代 JSI 代码应尽量避免手写裸分配。
Copy 还是 Share,不是性能题,而是系统设计题
这篇最容易被低估的一点是:零拷贝并不总比 copy 更好。
选择 copy 的场景
- 数据量小;
- 生命周期简单;
- 不希望共享期间互相影响;
- 希望边界清晰、bug 面更小。
选择 share 的场景
- 数据量大;
- 高频传输;
- 性能敏感;
- 你能清楚定义所有权和释放协议。
所以"要不要零拷贝"不是一个口号问题,而是一个 tradeoff:
- copy 增加 CPU / 内存带宽消耗;
- share 增加生命周期与并发复杂度。
原文给出的态度很稳健:默认先选更安全的 copy,只有在性能真的要求时再上 share。
ArrayBuffer 并不天然等于"可跨线程安全共享"
这一点在后续 Part 8、Part 9 非常关键,这里已经埋下伏笔。
即使底层数据是 ArrayBuffer,你也不能轻易推出:
- 可随便异步留存;
- 可随便后台线程继续访问;
- 可在 JS 侧对象消失后还继续持有裸地址。
因为:
- JS runtime 生命周期还在变;
- GC 何时清理你未必能控制;
- 背景线程使用时往往必须先 copy 出安全副本。
这就是为什么原文后面会说:同步边界上的 zero-copy,和异步 / 跨线程边界上的 zero-copy,不是同一道题。
我的补充理解
1. Part 6 是整个系列最关键的安全边界课
前面几篇更像"能做什么",这一篇开始回答"为什么会崩"。很多 JSI 问题其实都能追溯到这里:
- 裸指针拿太久;
- 共享对象死得太早;
- 以为字符串是 view,实际是 copy;
- 以为 buffer 还活着,实际已被释放。
2. shared_ptr 管的是对象,不是所有从对象里拿出来的地址
这是很多事故根源。对象本身被 shared_ptr 托管,不代表:
- 它内部 buffer 自动对所有线程安全;
- 你缓存出来的裸指针也自动安全;
- 外部异步任务不会越过生命周期边界。
一旦你把内部地址单独拿出来,新的所有权问题就开始了。
3. 性能优化和内存风险在 JSI 中是直接对价关系
Bridge 时代很多事做不了,所以很多低级所有权问题也被架构隐藏了。JSI 让你获得:
- 同步调用;
- 零拷贝;
- native-backed object;
同时也让你亲自承担:
- 谁释放;
- 谁共享;
- 谁跨线程;
- 谁在 GC 之后还存活。
这就是 JSI 的能力边界和风险边界是同一件事。
关键结论
- JavaScript GC heap 和 C++ native heap 是两套独立内存系统,JSI 只是在边界上建立受控交互。
shared_ptr是 JS 与 C++ 共享对象生命周期的关键桥梁,但它不能自动解决裸指针和跨线程问题。- 字符串在 JSI 中通常是复制传递,不应把它当成零拷贝通道。
ArrayBuffer才是高性能二进制数据交换的核心类型,但零拷贝会显著提高所有权复杂度。- 跨边界内存 bug 主要集中在 use-after-free、memory leak、raw allocation 三类。
- copy 和 share 的选择,本质上是"性能"与"生命周期复杂度"的权衡。
下一步适合看什么
下一篇 Part 7 会把这些代码真正放到 iOS 和 Android 上跑起来,主题从"内存边界"切到"平台 wiring"。
但从学习顺序上看,Part 8 线程篇和 Part 9 音频篇会真正把这里的所有权问题推到实战强度,因为一旦跨线程,Part 6 的风险会成倍放大。
参考链接
- 原文:React Native JSI Deep Dive --- Part 6: Memory Across Two Worlds --- Who Owns the Bytes Between JS and C++?
- React Native 源码:
jsi::ArrayBuffer - cppreference:
std::shared_ptr - cppreference:
std::weak_ptr