文章主旨
Part 4 解决的是"如何暴露一个 native 函数",而 Part 5 解决的是另一个更常见的问题:
如果我要暴露的不是一次性函数,而是一个带状态的 native 对象,该怎么办?
比如:
- 一个数据库连接;
- 一个同步 KV 存储;
- 一个音频会话;
- 一个有属性、有方法、还能自动释放资源的 C++ 实例。
JSI 给出的答案就是 jsi::HostObject。
为什么单纯 host function 不够
原文先回顾了 Part 3 里用 shared_ptr + 多个 lambda 共享状态的 KeyValueStore 示例。这种做法能用,但问题也很明显:
- 每个方法都得单独写一个 lambda;
- JavaScript 很难"看懂"这是一个什么对象;
- 不方便支持
storage.size这类属性访问; - 资源清理时机不够显式;
- 随着方法变多,样板代码迅速膨胀。
也就是说,host function 适合"按钮式调用",但不适合"对象式交互"。
这篇用一个很形象的类比来解释:
- host function 像自动售货机;
- HostObject 像柜台后的店员。
自动售货机是预先接线好的,按键即出结果。店员则可以根据你当前问的属性、方法和上下文动态响应。这就是 HostObject 的核心能力。
jsi::HostObject 的接口其实非常小
它只需要你理解三个入口:
cpp
class HostObject {
public:
virtual ~HostObject();
virtual jsi::Value get(jsi::Runtime& rt, const jsi::PropNameID& name);
virtual void set(jsi::Runtime& rt, const jsi::PropNameID& name, const jsi::Value& value);
virtual std::vector<jsi::PropNameID> getPropertyNames(jsi::Runtime& rt);
};
对应 JS 世界里的三个行为:
- 读属性:
obj.foo - 写属性:
obj.foo = 42 - 枚举属性:
Object.keys(obj)
这意味着 HostObject 本质上不是"把一个 C++ 类自动反射成 JS 类",而是:
让你在 C++ 层手动拦截 JS 对这个对象的属性访问。
所以可以把它类比为一个能力更小、但性能更直接的 C++ 版 Proxy。
get() 是 HostObject 的核心
绝大多数 HostObject 的重点都在 get()。
因为 JS 的这些访问最终都会走到它:
storage.getstorage.setstorage.sizestorage.clear
你会在 get() 里读取属性名,然后决定返回什么:
- 一个 number;
- 一个 string;
- 一个
jsi::Function; - 或者
undefined。
这意味着方法其实也是"属性读取的结果"。例如 JS 调:
js
storage.get("theme")
在底层会先发生:
- 读取
storage.get HostObject::get()返回一个jsi::Function- JS 再调用这个函数
这是理解 HostObject 的关键点:方法不是类里天然暴露出去的成员函数,而是你在属性访问时动态返回的 callable。
从一个简单 Counter 开始最能看清生命周期
原文先用一个最小 CounterHostObject 演示基本行为,这个步骤很有价值,因为它说明了 HostObject 真正新增的不是语法,而是"对象存活期间可以保留状态"。
与 Part 4 的函数式调用相比,这里多出来的能力是:
- 状态不再只活在一次调用里;
- JS 可以长期持有这个对象;
- 多次调用之间能共享底层 native 状态;
- 对象销毁时可以触发析构清理。
所以 HostObject 的真正定位是:
给 JavaScript 一个 native-backed object,而不是一组离散的 native functions。
一个完整 HostObject 一般长什么样
原文把 KeyValueStore 扩展成完整 HostObject 后,结构可以概括成:
- C++ 类内部持有实际状态。
get()里根据属性名分发。- 对方法类属性,返回
createFromHostFunction(...)造出来的函数。 - 对普通值属性,直接返回
jsi::Value。 - 通过
getPropertyNames()暴露可枚举 API。
这个模式和纯 host function 最大的区别是:
- pure function:安装时就定好了所有 callable;
- HostObject:每次属性读取时再决定返回什么。
原文也点出了一个成本:如果你在每次读取方法属性时都重新创建一个 jsi::Function,那会有额外分配开销。通常这种开销对普通业务模块是可以接受的,但在高频路径上就要更谨慎。
生命周期问题才是 HostObject 真正的难点
这篇的价值不只是"教你怎么 override get",而是明确讲清楚了生命周期链路。
典型安装方式:
cpp
auto obj = std::make_shared<MyHostObject>();
auto jsObj = jsi::Object::createFromHostObject(rt, obj);
这表示:
- C++ 侧有一个
shared_ptr<MyHostObject>; - JS runtime 也会保留对这个 HostObject 的引用;
- 只要 JS 或 C++ 任何一边还持有它,对象就不该析构;
- 最后一个引用释放后,析构函数才会跑。
这就是为什么 Part 5 的核心不是 get / set API,而是:
HostObject 让 shared_ptr 成为 JS 和 C++ 之间的生命周期协议。
一个常见坑:不要在返回的方法里裸捕获 this
原文特别强调了一个容易崩溃的模式:
- 在
get()中返回一个 lambda / host function; - 这个 lambda 里直接捕获裸
this; - 后面 JS 长时间持有这个方法;
- HostObject 本体先被释放了;
- 再次调用这个方法时就可能变成悬空指针。
更安全的思路是:
- 让 HostObject 本身由
shared_ptr管理; - 在需要长期活着的方法闭包里,持有安全的共享所有权;
- 或在更复杂场景里使用
shared_from_this()/weak_ptr模式。
这篇只是先把问题抛出来,真正完整的内存边界分析留给 Part 6。
HostObject、Host Function、普通 jsi::Object 的区别
原文做了一个很有用的对比,实操里可以这样记:
用 host function
适合:
- 无状态调用;
- 纯同步计算;
- API 表面简单;
- 一次调用一次返回。
用普通 jsi::Object + 多个函数属性
适合:
- 简单对象化包装;
- 方法数量少;
- 不太关心对象级生命周期语义。
用 HostObject
适合:
- 有 native 状态;
- 需要属性拦截;
- 需要更明确的生命周期管理;
- 需要把"一个 C++ 实例"暴露成"一个 JS 对象"。
这其实给了一个非常务实的判断框架:不是所有 JSI 模块都该上 HostObject,但只要你开始处理状态和资源,它通常会是更自然的模型。
原文给出的生产模式很实用:工厂函数
文章后面推荐的一个模式是:
- 不直接把 HostObject 单例粗暴挂全局;
- 而是暴露一个 factory function;
- 每次由 JS 主动创建对象实例。
例如:
js
const store = createStorage("/path/to/db");
这个方式的好处是:
- 实例化时机更清晰;
- 支持多实例;
- 销毁边界更自然;
- 更像正常 JS 模块 / 类实例的使用方式。
这对于数据库、会话、音频管线这类能力尤其重要。
我的补充理解
1. HostObject 才是大多数"同步原生对象能力"的真实承载体
很多人理解 JSI 时只记住"能同步调用 native function",但真正改变 React Native 能力边界的,往往不是单个函数,而是:
- JS 长期持有一个 native-backed object;
- 它内部有状态;
- 它的方法同步执行;
- 它的释放和资源生命周期可控。
这正是 HostObject 的价值。
2. HostObject 的本质不是类映射,而是属性调度器
不要把它想成 "C++ class 自动变 JS class"。更准确的理解是:
- JS 每次读属性;
- 引擎回调你的
get(); - 你自己决定这次返回什么。
这个认知非常重要,因为它决定了你会不会误判性能、枚举行为和方法创建时机。
3. 生命周期比 API 设计更重要
从工程角度看,HostObject 最危险的地方不是 if (name == "get") 这种分发逻辑,而是:
- 方法闭包持有谁;
- 谁在跨线程使用对象;
- 对象何时可能被 GC 间接释放;
- 析构时还能不能触碰 runtime。
这些问题如果不先想清楚,HostObject 越强,崩溃面也越大。
关键结论
- HostObject 用来把一个 C++ 实例暴露成 JavaScript 可持有、可读写、可调用的对象。
- 核心接口只有
get、set、getPropertyNames,其中get是绝对主角。 - 方法本质上是属性读取时动态返回的
jsi::Function,不是自动导出的成员函数。 - HostObject 适合需要状态、属性访问和资源生命周期管理的 native 能力。
shared_ptr是 HostObject 最重要的生命周期工具,错误的this捕获方式会直接导致悬空引用和 native crash。
下一步适合看什么
下一篇 Part 6 是 HostObject 的自然延伸,因为到这里你已经绕不过一个问题:
JS 和 C++ 到底谁拥有这些内存?
一旦开始处理:
shared_ptr- buffer
- string copy
- ArrayBuffer 零拷贝
- native 资源释放
你就正式进入 JSI 最容易出事故的区域了。
参考链接
- React Native 源码:
jsi::HostObject