React Native JSI 深入剖析 — 第 5 部分中文技术整理:用 HostObject 把 C++ 类暴露给 JavaScript

文章主旨

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.get
  • storage.set
  • storage.size
  • storage.clear

你会在 get() 里读取属性名,然后决定返回什么:

  • 一个 number;
  • 一个 string;
  • 一个 jsi::Function
  • 或者 undefined

这意味着方法其实也是"属性读取的结果"。例如 JS 调:

js 复制代码
storage.get("theme")

在底层会先发生:

  1. 读取 storage.get
  2. HostObject::get() 返回一个 jsi::Function
  3. JS 再调用这个函数

这是理解 HostObject 的关键点:方法不是类里天然暴露出去的成员函数,而是你在属性访问时动态返回的 callable。

从一个简单 Counter 开始最能看清生命周期

原文先用一个最小 CounterHostObject 演示基本行为,这个步骤很有价值,因为它说明了 HostObject 真正新增的不是语法,而是"对象存活期间可以保留状态"。

与 Part 4 的函数式调用相比,这里多出来的能力是:

  • 状态不再只活在一次调用里;
  • JS 可以长期持有这个对象;
  • 多次调用之间能共享底层 native 状态;
  • 对象销毁时可以触发析构清理。

所以 HostObject 的真正定位是:

给 JavaScript 一个 native-backed object,而不是一组离散的 native functions。

一个完整 HostObject 一般长什么样

原文把 KeyValueStore 扩展成完整 HostObject 后,结构可以概括成:

  1. C++ 类内部持有实际状态。
  2. get() 里根据属性名分发。
  3. 对方法类属性,返回 createFromHostFunction(...) 造出来的函数。
  4. 对普通值属性,直接返回 jsi::Value
  5. 通过 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 可持有、可读写、可调用的对象。
  • 核心接口只有 getsetgetPropertyNames,其中 get 是绝对主角。
  • 方法本质上是属性读取时动态返回的 jsi::Function,不是自动导出的成员函数。
  • HostObject 适合需要状态、属性访问和资源生命周期管理的 native 能力。
  • shared_ptr 是 HostObject 最重要的生命周期工具,错误的 this 捕获方式会直接导致悬空引用和 native crash。

下一步适合看什么

下一篇 Part 6 是 HostObject 的自然延伸,因为到这里你已经绕不过一个问题:

JS 和 C++ 到底谁拥有这些内存?

一旦开始处理:

  • shared_ptr
  • buffer
  • string copy
  • ArrayBuffer 零拷贝
  • native 资源释放

你就正式进入 JSI 最容易出事故的区域了。

参考链接

相关推荐
weixin_3823952316 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__16 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.17 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~18 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华19 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子21 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅21 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni21 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学1 天前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端