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 最容易出事故的区域了。

参考链接

相关推荐
胡萝卜术1 小时前
滑动窗口最大值:从暴力到单调队列,层层优化全解析
前端·javascript·面试
fluffyox1 小时前
Notion 的公式栏里,藏着一台虚拟机——逆向 + 用 600 行 JS 复刻它的编译器与栈式 VM
前端
kyriewen3 小时前
2026 年了,这 6 个 npm 包可以卸载了——浏览器原生 API 已经能替代
前端·javascript·npm
Csvn5 小时前
Monorepo 迁移血泪史:从 Multi-Repo 到 Turborepo,这 3 个坑我帮你踩完了
前端
星栈5 小时前
Dioxus 多页面怎么做:`dioxus-router`、嵌套路由、`Outlet` 和页面组织,一篇给你讲顺
前端·rust·前端框架
用户987409238876 小时前
用 Remotion + edge-tts 打造中文教学视频全自动流水线
前端
风骏时光牛马6 小时前
Less前端工程化实战:变量混合器与项目样式分层落地
前端
假如让我当三天老蒯6 小时前
Options API(选项式 API) 和 Composition API(组合式 API)
前端·vue.js·面试
SameX6 小时前
iOS 独立开发实践:用 MapKit + 像素渲染实现 Citywalk 轨迹地图 App「雁过留痕」
前端