【翻译】React Native JSI 深度解析(第 3 篇):面向 JavaScript 开发者的 C++

React Native JSI 深度解析(第 3 篇):面向 JavaScript 开发者的 C++

"抽象的目的不是含糊其辞,而是创建一个新的语义层,在这个层里你可以做到绝对精确。"

--- Edsger W. Dijkstra,The Humble Programmer,1972

导读: 你不需要学完整门 C++ 才能写 JSI 原生模块。你只需要掌握 5 个概念:栈与堆、引用与指针、RAII、智能指针、Lambda。本文只讲这部分,而且会用你已经熟悉的 JavaScript 语境来解释。读完后,你看 C++ 会像看 TypeScript 一样:不必认识每个关键字,但能读懂每个意图

系列:React Native JSI 深度解析(12 篇)
第 1 篇:React Native 架构------线程、Hermes 与事件循环 | 第 2 篇:React Native Bridge 与 JSI------到底变了什么 | 第 3 篇:面向 JavaScript 开发者的 C++(你在这里) | 第 4 篇:你的第一个 React Native JSI 函数 | 第 5 篇:HostObjects------把 C++ 类暴露给 JavaScript | 第 6 篇:内存所有权 | 第 7 篇:平台接线 | 第 8 篇:线程与异步 | 第 9 篇:实时音频管线 | 第 10 篇:存储引擎 | 第 11 篇:TurboModules vs Pure JSI vs Pure C++ | 第 12 篇:生产调试与陷阱


问题:C++ 看起来像"痛苦制造语言"

如果你一直写 JavaScript 或 TypeScript,第一次看到 C++ 版 JSI 函数可能是这样:

一个 JSI 函数长什么样:

cpp 复制代码
static jsi::Value multiply(
    jsi::Runtime& rt,
    const jsi::Value& thisVal,
    const jsi::Value* args,
    size_t count) {
  double a = args[0].asNumber();
  double b = args[1].asNumber();
  return jsi::Value(a * b);
}

你看到 &*constsize_t,第一反应可能是:我得再学一门语言。

但再看一眼,去掉符号后,它就是"接收两个数并返回乘积"的函数。&* 本质只是在回答一件事:数据归谁管,存在哪。

这就是核心思维转换。JavaScript 用 GC 把内存管理细节藏起来;C++ 要你显式声明。除此之外,类、循环、分支、字符串这些,整体都和你预期差不多。

本文只讲你在 JSI 模块中一定会遇到的 5 个 C++ 概念:不讲模板套模板,不讲运算符重载,不讲多重继承。


概念 1:栈 vs 堆(数据住在哪里)

在 JavaScript 里你几乎不会想变量住在哪。你写 const x = 42,引擎会处理后面的事。

在 C++ 中,数据主要在两个地方:栈(stack)堆(heap),而且由你决定。(JSI 场景下,理解栈和堆就够用。)

栈(Stack)

栈内存快、自动管理。函数运行时,本地变量在栈上;函数返回时自动销毁,不需要手动清理。

栈上分配:自动生命周期

cpp 复制代码
void greet() {
    int count = 42;            // 在栈上
    std::string name = "JSI";  // 变量在栈上(字符串内部内容可能在堆上)
    // 使用 count 和 name...
}  // ← 这里自动销毁

JavaScript 类比是函数内部 let 的生命周期:它在函数执行期间存在,之后变为可被垃圾回收。

但关键差异是:C++ 栈对象销毁是立即且确定的。它不会等"将来某次 GC 轮到它",而是在右花括号处就发生。每一次都如此,且有语言层面的保证。

堆(Heap)

堆用于"需要活过创建它的函数"的数据。JavaScript 的对象、数组、闭包基本都可视为堆上数据,由 GC 回收。

C++ 里你可以用 new 在堆上分配,并用 delete 手动释放:

堆上分配:手动生命周期 ⚠️

cpp 复制代码
void createBuffer() {
    int* data = new int[1024];  // 在堆上分配
    // 使用 data...
    delete[] data;              // 必须手动释放
}  // 忘记 delete[] 就泄漏

思考: 如果 new 后、delete 前抛异常,会怎样?delete 不会执行,直接泄漏。
这就是手动内存管理的根本问题,也是现代 C++ 几乎不用裸 new/delete 的原因。后面会用 RAII 解决。

心智模型:

txt 复制代码
┌─────────────────────────────────────────────────────────┐
│                        STACK                             │
│  快,自动,固定大小;函数返回即销毁。                  │
│  C++ 常见用途:局部变量、函数参数。                     │
├─────────────────────────────────────────────────────────┤
│                        HEAP                              │
│  相对慢,动态;手动或由智能指针管理。                  │
│  不释放就一直活着(或泄漏)。                          │
│  C++ 常见用途:需要跨函数生命周期的数据。              │
└─────────────────────────────────────────────────────────┘

图 1:栈 vs 堆。JavaScript 把这层差异隐藏在垃圾回收器之后;C++ 要求你显式做出选择。

JSI 模块里,你通常会大量使用栈对象 + 智能指针。写得好的现代 C++ 里,裸 new/delete 很少出现。


概念 2:引用与指针(数据别名)

在 JavaScript 里,把对象传进函数时,函数拿到的是引用 的副本:它可以修改对象的属性,但给参数重新赋值不会影响调用方变量。(从技术上说这叫 "pass-by-sharing",并不是 C++ 语境里的真正"按引用传递")

但在"修改对象内容"这个场景里,它的体感确实很像按引用传递:

JavaScript:对象变更对调用方可见

js 复制代码
function addItem(list) {
  list.push("new item");
}

const myList = ["a", "b"];
addItem(myList);
console.log(myList); // ['a', 'b', 'new item']

C++ 会显式让你选:按值(copy)、按引用(alias)、按指针(地址)传递。这就是 &* 的意义。

按值传递(Copy)

按值传递:产生副本

cpp 复制代码
void process(std::string text) {   // 拷贝
    text += " modified";           // 只改副本
}

std::string original = "hello";
process(original);
// original 仍是 "hello"

这和 JavaScript 里原始值的传递行为很像:let x = 5; foo(x); 传的是副本。

按引用传递(&

按引用传递:原数据别名

cpp 复制代码
void process(std::string& text) {  // 引用
    text += " modified";           // 改原值
}

类型后面的 & 表示"这不是副本,而是同一份数据的另一个名字"。最接近的 JavaScript 类比是把对象传进函数:函数能改对象属性,因为它拿到的是同一份数据的引用。

但 C++ 引用还更进一步:如果你在函数里给引用参数重新赋值(例如 text = "new value"),会直接改调用方变量本体;而 JavaScript 里在函数内部 param = newValue 不会影响调用方。

常量引用(const &

常量引用:只读别名

cpp 复制代码
void print(const std::string& text) {
    std::cout << text;       // 可读
    // text += " nope";      // 编译错误
}

这是 JSI 代码里最常见的模式之一:当函数接收的数据只需要读取、不需要修改时,就会使用 const &。这样既能避免拷贝开销,又能防止意外修改。

关键理解: 在 JSI 函数签名里看到 const jsi::Value& 时,可以读作:"我在本次调用期间借用这个值;我不会修改它,也不会在返回后持有它。"const 是对编译器的承诺,也是对代码阅读者的承诺。

指针(*

指针保存的是内存地址。它比引用更底层------引用通常可由指针实现,但语义更安全(不能是空、不能改绑到别处)。

指针:地址操作语义

cpp 复制代码
int value = 42;
int* ptr = &value;
std::cout << *ptr;    // 42

你会在 JSI 函数签名中看到指针:

cpp 复制代码
jsi::Value myFunction(
    jsi::Runtime& rt,
    const jsi::Value& thisVal,
    const jsi::Value* args,
    size_t count
) {
    double x = args[0].asNumber();
    // 其他处理...
}

args 参数是数组首元素的指针。args[0] 是第一个参数,args[1] 是第二个参数。count 参数告诉你一共有多少个参数。

这是 C 风格数组传参:没有 .length 属性,所以长度需要单独传入。

速查表:

符号 含义 JS 类比
Type x 按值(拷贝) 原始值传参
Type& x 引用(别名) 对象可变更的效果类比
const Type& x 只读引用 只读借用
Type* x 指针(地址) 无直接等价
&x 取地址 无直接等价
*x 解引用 无直接等价

图 2:C++ 参数传递符号。& 一符两义:在类型声明中表示"引用",在表达式中表示"取地址"。

易错点(Gotcha): & 符号会随上下文变化而有两种完全不同的含义。
类型声明* 里(如 std::string& text),它表示"引用到(reference to)";
表达式 里(如 int* ptr = &value),它表示"取地址(address of)"。
这几乎会绊住每个刚学 C++ 的 JavaScript 开发者。看到 & 时,先判断它是挨着类型,还是挨着变量名。*


概念 3:RAII(销毁即清理)

RAII(Resource Acquisition Is Initialization)是 JSI 开发中最重要的 C++ 概念。这个名字可能是计算机科学里最"劝退"的命名之一,但它背后的思想其实很简单。

在 JavaScript 中,你通常要手写清理代码:

JavaScript 常见手动清理:

js 复制代码
function readFile(path) {
  const handle = openFile(path);
  try {
    return handle.read();
  } finally {
    handle.close();
  }
}

如果你忘了写 finally,文件句柄就泄漏了;如果在 close() 前抛异常且不在 try 覆盖范围里,也会泄漏。这种写法很脆弱。

在 C++ 里,RAII 的含义是:构造函数负责获取资源,析构函数负责释放资源。

由于对象离开作用域时(包括栈展开过程)析构函数会自动运行,所以清理是有保证的------即使抛出异常也一样。

C++:RAII 让清理自动发生

cpp 复制代码
class FileHandle {
    FILE* file_;
public:
    FileHandle(const char* path) : file_(fopen(path, "r")) {
        if (!file_) throw std::runtime_error("Failed to open file");
    }
    ~FileHandle() { fclose(file_); }
};

std::string readFile(const char* path) {
    FileHandle handle(path);
    auto content = handle.read();
    return content;
}  // 自动调用析构,保证 close

~FileHandle()析构函数 :对象销毁时自动运行。对栈对象来说,通常是离开作用域(遇到 })时;对堆对象来说,是 delete 调用时(或智能指针判断到该释放时)。

关键洞察: RAII 关注的并不只是文件,而是任何资源------内存、网络连接、锁、GPU 缓冲区、音频会话。这个模式始终一致:在构造函数中获取,在析构函数中释放,并让作用域来决定生命周期。
在 JSI 模块中,HostObject 会用 RAII 管理它的 C++ 状态:当 JavaScript 的垃圾回收器回收 HostObject 时,C++ 析构函数会运行,并清理对应的原生资源。

心智模型:

txt 复制代码
JavaScript:                          C++ (RAII):

  const x = acquire();                {
  try {                                 Resource x(...);  // 获取
    use(x);                             use(x);
  } finally {                        }  // ← 析构自动释放
    release(x);                         //   即使异常也执行
  }

图 3:RAII 消除了手写清理。右花括号本身就是 finally

RAII 之所以对 JSI 特别重要,是因为原生模块会管理很多 JavaScript 垃圾回收器并不了解的资源------比如音频缓冲区、文件句柄、数据库连接、原生线程池。RAII 能保证这些资源以"确定性"的方式被清理,而不是等"GC 哪天有空再处理"。


概念 4:智能指针(堆内存自动管理)

new / delete 是 C++ 对 C 语言 malloc / free 的类型安全版本。与 malloc/free 不同,new 会调用构造函数,delete 会调用析构函数;但只要手写,就仍然容易出错。

现代 C++ 的主流做法是使用智能指针 :它们是对堆指针的 RAII 封装,会在不再需要时自动 delete

你只需要掌握两种智能指针。可以把它们理解成两种"所有权策略"。

std::unique_ptr(独占所有权)

unique_ptr 对其数据拥有独占所有权,其他对象不能共同拥有。unique_ptr 被销毁时,底层数据会被释放。你不能拷贝它------只能对它做 move(转移所有权)。

cpp 复制代码
#include <memory>

void example() {
    // 创建 unique_ptr:它独占 AudioBuffer
    auto buffer = std::make_unique<AudioBuffer>(1024);
    buffer->fill(0.0f);

    // auto copy = buffer;  // ❌ 不能拷贝
    auto moved = std::move(buffer); // ✓ 转移所有权
    // buffer 现在是 nullptr,数据由 moved 持有
}
// ← moved 销毁时,AudioBuffer 自动释放

JavaScript 类比:想象一个"不可共享"的引用。任意时刻只能有一个变量指向这份数据;要交给别人只能 move,原变量随即变成 null

txt 复制代码
unique_ptr 所有权转移:

  auto a = make_unique<X>();     a ──────▶ [X on heap]

  auto b = std::move(a);         a ──▶ nullptr
                                 b ──────▶ [X on heap]

  // b 离开作用域               b 销毁 -> [X freed]

图 4:unique_ptr 的所有权转移。同一时刻只能有一个指针拥有该数据。move 会转移所有权,并将源指针置为空。

std::shared_ptr(共享所有权)

shared_ptr 允许多个拥有者共享同一份数据。它内部维护一个引用计数:每拷贝一次计数加一,每销毁一个持有者计数减一。当计数降到零时,底层数据会被释放。

cpp 复制代码
#include <memory>

void example() {
    auto config = std::make_shared<AppConfig>(); // 引用计数=1
    auto copy1 = config;                         // 引用计数=2
    auto copy2 = config;                         // 引用计数=3
    copy1.reset();                               // 引用计数=2
    copy2.reset();                               // 引用计数=1
} // 引用计数=0 后释放

它最接近 JavaScript 的 GC 心智模型:对象只要"仍有人引用"就存活。

区别是:shared_ptr 是确定性的引用计数(计数归零立刻释放);JavaScript 是 tracing GC("未来某次 GC"释放)。

txt 复制代码
shared_ptr 引用计数:

  auto a = make_shared<X>();     a ──────▶ [X] 引用计数: 1
  auto b = a;                    a ──────▶ [X] 引用计数: 2
                                 b ──────┘
  a.reset();                     b ──────▶ [X] 引用计数: 1
  b.reset();                               [X] 引用计数: 0 -> 释放

图 5:shared_ptr 的引用计数。多个指针可指向同一份数据;当最后一个指针释放时,数据才会被销毁。

JSI 里该选哪一个?

智能指针 适用场景 JSI 示例
unique_ptr 单一所有者、无需共享 内部缓冲区、临时计算结果
shared_ptr 多方持有,或需要暴露给 JS HostObject(JS GC 与 C++ 都要持有)

对 JSI 来说,真正关键的是 shared_ptr。当你创建 HostObject(即暴露给 JavaScript 的 C++ 对象)时,它通常会被包裹在 std::shared_ptr 里。JavaScript 垃圾回收器会持有一个引用,而你的 C++ 代码也可能持有其他引用。只有当 JS 与 C++ 两侧都释放各自引用后,HostObject 才会被销毁。

HostObject 使用 shared_ptr(第 5 篇预告)

cpp 复制代码
// HostObject 一律使用 shared_ptr ------ JS GC 会持有其中一个引用
auto storage = std::make_shared<StorageHostObject>(dbPath);
runtime.global().setProperty(
    runtime, "storage",
    jsi::Object::createFromHostObject(runtime, storage)
);
// 现在:JS(通过 GC)持有一个引用,C++ 侧也持有 `storage`
// 只有当双方都释放后,StorageHostObject 才会被销毁

易错点(Gotcha): shared_ptr 是有额外开销的------它的引用计数是原子整数(支持线程安全的递增/递减),并且每个 shared_ptr 都比裸指针更大(因为它携带控制块)。在热路径和实时代码中,应优先考虑 unique_ptr
我们会在第 8、9 篇构建多线程与音频管线代码时看到,这个差异为什么重要。


概念 5:Lambda(C++ 闭包)

Lambda 是你最容易一眼认出来的 C\+\+ 概念。它本质上就是闭包------能够从其外围作用域捕获变量的匿名函数。

JavaScript 闭包

js 复制代码
function makeCounter() {
  let count = 0;
  return () => ++count;
}

C\+\+ Lambda:同样的模式

cpp 复制代码
auto makeCounter() {
    int count = 0;
    return [count]() mutable { return ++count; };
}

语法看起来不一样,但可观察结果相同:连续调用返回函数 3 次,你会得到 1、2、3。

内部机制不同------JS 捕获的是变量绑定(同作用域闭包可共享),而 C\+\+ 的 [count] mutable 是捕获私有副本------但在"返回单个计数器"这个场景下结果一致。

语法骨架:

cpp 复制代码
[capture](parameters) -> return_type { body }

捕获列表 [...] 正是 C\+\+ lambda 与 JavaScript 闭包的关键区别。在 JavaScript 里,闭包会自动捕获外层作用域中的变量绑定------它能够观察到这些变量后续的变化,这在行为上类似于 C\+\+ 的"按引用捕获"(但 JS 通过 GC 保活作用域,所以不存在悬空引用风险)。在 C\+\+ 里,你必须显式选择"捕获什么"以及"如何捕获"。

捕获模式

cpp 复制代码
int x = 10;
std::string name = "JSI";

auto byValue    = [x]()        { return x; };
auto byRef      = [&x]()       { return x; };
auto allValue   = [=]()        { return x; };
auto allRef     = [&]()        { return x; };
auto mixed      = [x, &name]() { return name + "!"; };
捕获方式 语法 JS 类比 行为
按值捕获 [x] const x_copy = x 后使用 x_copy 快照语义,外部 x 变化不会影响 lambda
按引用捕获 [&x] 最接近 JS 闭包体验 活别名语义,可观察/修改外部 x
全部按值 [=] 无直接等价 复制函数体里用到的所有外部变量
全部按引用 [&] 接近 JS 默认闭包 以引用方式捕获函数体里用到的所有变量

图 6:Lambda 捕获模式。JavaScript 闭包总是共享外层作用域的变量绑定;C++ 则要求你显式做出选择------而这个选择会直接影响线程安全。

为什么"捕获"对 JSI 尤其关键?

这就是 JSI 关联变得关键的地方:当你创建一个 JSI host function 时,通常会使用 lambda。

JSI 中最重要的模式是:按值捕获 shared_ptr,确保对象生命周期足够长。

带 Lambda 捕获的 JSI host function

cpp 复制代码
void install(jsi::Runtime& runtime, std::shared_ptr<Database> db) {
    auto get = jsi::Function::createFromHostFunction(
        runtime,
        jsi::PropNameID::forAscii(runtime, "get"),
        1,
        [db](jsi::Runtime& rt,
             const jsi::Value& thisVal,
             const jsi::Value* args,
             size_t count) -> jsi::Value {
            auto key = args[0].asString(rt).utf8(rt);
            auto result = db->get(key);
            return jsi::String::createFromUtf8(rt, result);
        }
    );
    runtime.global().setProperty(runtime, "dbGet", std::move(get));
}

注意:这里 lambda 按 捕获 db。但 dbshared_ptr,按值捕获实际上是"复制 shared_ptr 本身",会增加引用计数。于是 lambda 获得数据库对象的共享所有权。即使外层 db 变量离开作用域,lambda 里那份副本仍能让对象存活。

想一想: 如果我们不是按值捕获 db[db]),而是按引用捕获([&db]),会发生什么?install 函数返回后,作为局部变量的 db 会被销毁,而 lambda 会持有一个悬空引用------也就是指向一块已不存在内存的指针。下一次 JavaScript 调用 dbGet() 时,就会崩溃。
这就是为什么 JSI 的 lambda 几乎总是按值捕获 shared_ptr,而不是按引用捕获。

这种模式------在 JSI 的 lambda 里按值捕获 shared_ptr------几乎出现在每一个原生模块中。它正是 C++ 对象能够"按 JavaScript 需要的时长持续存活"的关键机制。


Move 语义:转移所有权而非复制

还有一个概念能把前面的内容全部串起来。你已经在 unique_ptr 场景看过 std::move,现在来理解它到底做了什么。

在 JavaScript 里,对象赋值不会复制对象本体:

JavaScript:对象是共享的,不是拷贝的

js 复制代码
const a = { data: [1, 2, 3] };
const b = a; // b 和 a 指向同一个对象
b.data.push(4); // a.data 也会变成 [1, 2, 3, 4]

在 C++ 里,对象赋值默认会发生拷贝

C++:对象默认按值拷贝

cpp 复制代码
std::vector<int> a = {1, 2, 3};
std::vector<int> b = a;     // b 是副本,a 和 b 相互独立
b.push_back(4);             // a 仍然是 {1, 2, 3}

拷贝很安全,但可能很贵。如果 a 里有 1MB 数据,b = a 就会真的复制这 1MB。
std::move 的意思是:"我不再需要 a 了,把内部资源直接转移给 b,不要拷贝。"

Move:不拷贝,直接转移

cpp 复制代码
std::vector<int> a = {1, 2, 3};
std::vector<int> b = std::move(a);  // b 接管 a 的内部缓冲区
// a 进入 moved-from 状态:仍然有效,但值未指定(通常为空)
// b 持有 {1, 2, 3},过程没有深拷贝

可以把它类比成:

普通拷贝 = 复印一份 100 页文档;

move = 直接把文档递给别人,几乎瞬时,但你自己不再持有原件。

txt 复制代码
Copy:    a ──▶ [1,2,3]         b ──▶ [1,2,3]    (存在两份数据)

Move:    a ──▶ []              b ──▶ [1,2,3]    (数据转移,无复制)

图 7:Copy vs Move。Copy 复制数据,Move 转移所有权。源对象会留在"有效但值未指定"的状态(通常为空)。

你会在 JSI 代码里看到 std::move 的常见场景:

  • unique_ptr 转移给新的拥有者
  • 把大对象传入函数时避免拷贝
  • 高效返回构造好的对象

JSI 场景中的 move

cpp 复制代码
// 把 JSI 函数 move 到属性里(无需复制)
auto fn = jsi::Function::createFromHostFunction(rt, name, 0, callback);
rt.global().setProperty(rt, "myFunc", std::move(fn));
// fn 现在是空的,runtime.global() 接管了所有权

串起来看:一段真实 JSI 代码

把这 5 个概念放进一段真实 JSI 模块代码里看。下面是一个简化版,风格接近你在 react-native-mmkv 这类库里会见到的写法:

一个完整的迷你 JSI 模块:所有核心概念都在里面

cpp 复制代码
#include <jsi/jsi.h>
#include <memory>
#include <string>
#include <unordered_map>

using namespace facebook;

// 一个简单的内存键值存储
class KeyValueStore {
public:
    // const&:只读引用,避免拷贝
    void set(const std::string& key, const std::string& value) {
        data_[key] = value;
    }

    // const 成员函数:不修改对象状态
    std::string get(const std::string& key) const {
        auto it = data_.find(key);
        if (it != data_.end()) return it->second;
        return "";
    }

private:
    // 成员随 KeyValueStore 生命周期销毁(RAII)
    std::unordered_map<std::string, std::string> data_;
};  // 析构时自动释放 data_(RAII)

// rt 是引用:借用 runtime,不拥有 runtime
void installStorage(jsi::Runtime& rt) {
    // shared_ptr:JS GC 和 C++ 都可能持有
    auto store = std::make_shared<KeyValueStore>();

    // set 函数:lambda 按值捕获 store(shared_ptr 拷贝,引用计数+1)
    auto setFn = jsi::Function::createFromHostFunction(
        rt, jsi::PropNameID::forAscii(rt, "set"), 2,
        [store](jsi::Runtime& rt, const jsi::Value&,
                const jsi::Value* args, size_t count) -> jsi::Value {
            auto key = args[0].asString(rt).utf8(rt);  // jsi::String -> std::string
            auto val = args[1].asString(rt).utf8(rt);
            store->set(key, val);                      // 使用捕获到的 shared_ptr
            return jsi::Value::undefined();
        }
    );

    // get 函数:同样的捕获模式
    auto getFn = jsi::Function::createFromHostFunction(
        rt, jsi::PropNameID::forAscii(rt, "get"), 1,
        [store](jsi::Runtime& rt, const jsi::Value&,
                const jsi::Value* args, size_t count) -> jsi::Value {
            auto key = args[0].asString(rt).utf8(rt);
            auto result = store->get(key);
            return jsi::String::createFromUtf8(rt, result);
        }
    );

    // 安装到 JS 全局作用域:move 转移所有权,避免不必要复制
    auto storage = jsi::Object(rt);
    storage.setProperty(rt, "set", std::move(setFn));  // move:转移所有权
    storage.setProperty(rt, "get", std::move(getFn));
    rt.global().setProperty(rt, "storage", std::move(storage));
}

在 JavaScript 里这样调用:

js 复制代码
storage.set("theme", "dark");
const theme = storage.get("theme");
console.log(theme);

输出:

json 复制代码
"dark"

这段代码里包含了本文所有关键概念:

代码位置 概念 发生了什么
jsi::Runtime& rt 引用 借用 runtime,不拥有它
const jsi::Value& 常量引用 只读访问 this
const jsi::Value* args 指针 指向参数数组
std::make_shared() 智能指针 在堆上分配并采用共享所有权
[store](...) { ... } Lambda + 捕获 按值捕获 shared_ptr 闭包
std::move(setFn) Move 把函数所有权转移给对象
~KeyValueStore()(隐式) RAII 销毁时自动释放 data_

你暂时可以不学的 C++ 内容

C++ 非常庞大。针对 JSI 开发,下面这些内容你现在可以放心先跳过:

C++ 特性 为什么当前可忽略
高级模板 JSI 内部会用,但你通常不必自己写
多重继承 JSI 场景常见单继承
高阶运算符重载 模块开发很少需要自己定义
const_cast / reinterpret_cast 系统层偶尔有用,JSI 入门阶段通常不需要
手写 new / delete 优先 make_unique / make_shared
复杂宏逻辑 除平台 #ifdef 外尽量少用

如果你在第三方原生模块里看到这些高级特性,通常也不影响你理解周边 JSI 代码的核心逻辑。


关键结论

  • 栈 vs 堆。 栈内存是自动的:函数开始时分配,函数返回时释放。堆内存生命周期更长,需要管理。JSI 里通常用智能指针来管理堆内存。
  • 引用(&)与指针(*)。 引用是别名,即已有数据的另一个名字。const & 表示"只读借用"。指针保存内存地址。JSI 里常见 jsi::Runtime&(借用 runtime)和 const jsi::Value*(参数数组指针)。
  • RAII。 构造函数获取资源,析构函数释放资源,生命周期由作用域决定。这是 C++ 对 try/finally 的语言级答案,而且不会被忘记。每个 HostObject 都依赖 RAII 在 JS 垃圾回收后清理原生资源。
  • 智能指针。 unique_ptr = 单所有者、自动清理;shared_ptr = 通过引用计数共享所有权。HostObject 通常使用 shared_ptr,因为 JS GC 与 C++ 代码都可能持有同一对象。
  • Lambda 显式捕获。 不同于 JavaScript 闭包默认共享外层绑定,C++ lambda 必须显式声明捕获内容和方式。JSI 最关键模式是:在 lambda 里按值捕获 shared_ptr,让原生对象在 JS 仍需访问时保持存活。

Reading the Crash(回看崩溃栈)

再看第 1 篇的 crash trace,你现在已经能读懂其中的 C++ 符号

  • audio::TxRingBuffer::push(uint8_t const*, unsigned long, long)audio 命名空间下 TxRingBuffer 类的 push 方法,参数是只读字节指针、长度、时间戳。你现在知道 uint8_t const* 表示"指向原始字节的只读指针"。
  • std::__ndk1::shared_ptr<audio::AudioPipelineHostObject>::~shared_ptr()shared_ptr 析构函数。~ 代表析构(RAII 清理发生)。模板参数告诉你它持有的是 AudioPipelineHostObject。当它运行时,说明引用计数归零了,最后一个拥有者已释放。
  • audio::TxRingBuffer::~TxRingBuffer():环形缓冲区的析构函数。它在 HostObject 析构过程中被调用,意味着 pipeline 持有 ring buffer,销毁 pipeline 就会连带销毁 buffer。

现在析构链条就很清楚了:shared_ptr 释放 -> AudioPipelineHostObject 析构 -> TxRingBuffer 析构。与此同时,CaptureEncoderThread::processFrame 仍在调用 TxRingBuffer::push。缓冲区正在一个线程上被销毁,而另一个线程还在向它写入。这就是典型的 use-after-free------而你现在已经有足够的 C++ 认知,能从这些符号里直接看出来。

当前系统视图:

txt 复制代码
 JS Thread            UI Thread           Native Thread
┌───────────────┐   ┌───────────────┐   ┌───────────────┐
│    Hermes     │   │   Platform    │   │    C++ code   │
│               │   │               │   │               │
│  ── JSI ──────┼───┼───────────────┼──▶│  stack / heap │  ← NEW
│               │   │               │   │  shared_ptr   │  ← NEW
│               │   │               │   │  RAII + dtors │  ← NEW
└───────────────┘   └───────────────┘   └───────────────┘

Frequently Asked Questions

React Native JSI 需要掌握哪些 C++?

5 个核心概念:栈/堆内存、引用(&)与指针(*)、RAII(通过析构自动清理资源)、智能指针(unique_ptrshared_ptr)、以及带显式捕获列表的 lambda。你不需要先掌握模板高级玩法、多重继承、运算符重载。

RAII 为什么对 JSI 重要?

RAII(Resource Acquisition Is Initialization)意味着:构造时获取资源,离开作用域时通过析构自动释放资源。在 JSI 中,HostObject 借助 RAII 来清理原生资源(文件句柄、缓冲区、连接等),不需要手动 close(),也不依赖"未来某次 GC 再说"。

unique_ptrshared_ptr 区别是什么?

unique_ptr 是独占所有权:同一时间只有一个指针拥有对象,通过 std::move 转移所有权。shared_ptr 是引用计数共享所有权:最后一个引用释放时对象才销毁。JSI HostObject 常用 shared_ptr,因为 JavaScript GC 和 C++ 侧都可能同时持有引用。

为什么 JSI Lambda 常按值捕获 shared_ptr

按值捕获 shared_ptr 会复制指针并增加引用计数,从而保证 lambda 存在期间原生对象持续存活。若按引用捕获([&db]),外层变量离开作用域后就会留下悬空引用,下次 JS 调用函数时就可能崩溃。


下一篇预告

你现在已经掌握了 C++ 的核心词汇:数据住在哪(栈/堆)、怎么借用(&)、怎么管理(unique_ptr / shared_ptr)、怎么清理(RAII)、怎么写闭包(显式捕获 lambda)。

第 4 篇:你的第一个 React Native JSI 函数 中,我们会把它们真正拼起来:从零写一个 JSI 函数,完成 runtime 注册、参数校验、错误处理,并从 JavaScript 端调用它。不用 boilerplate 生成器,不用 codegen,就用最原生的 JSI。

第 3 篇给你"词汇",第 4 篇给你"动词"。


References & Further Reading

  1. cppreference --- std::unique_ptr
  2. cppreference --- std::shared_ptr
  3. cppreference --- RAII
  4. cppreference --- Lambda expressions
  5. cppreference --- Move semantics
  6. C++ Core Guidelines --- Bjarne Stroustrup & Herb Sutter
  7. JSI Header --- jsi.h (facebook/react-native)

Quick Reference

C++ 与 JavaScript 概念对照

JavaScript C++ 对应 关键差异
let x = obj auto x = obj(拷贝)或 auto& x = obj(引用) C++ 默认拷贝;& 才是避免拷贝
Garbage Collected 栈(自动)或堆(new/智能指针) 栈在 } 处立即结束;堆需显式或托管释放
闭包自动捕获 Lambda 显式捕获 [...] [=] 全部按值,[&] 全部按引用
undefined 无直接等价 C++ 未初始化内存属于未定义行为

智能指针速查

类型 是否拥有资源 是否可拷贝 适用场景
std::unique_ptr 是(独占) 否(仅 move) 单所有者、无需共享
std::shared_ptr 是(共享) 是(引用计数) 多所有者,尤其 JS ↔ C++ 边界
std::weak_ptr 否(观察者) N/A 断环、探测对象是否仍存活

RAII 模式

cpp 复制代码
{
    auto ptr = std::make_shared<MyClass>();  // 构造函数执行
    // ... 使用 ptr ...
}   // 析构函数在此保证执行(即使异常)

系列:React Native JSI 深度解析(12 篇)
第 1 篇:React Native 架构------线程、Hermes 与事件循环 | 第 2 篇:React Native Bridge 与 JSI------到底变了什么 | 第 3 篇:面向 JavaScript 开发者的 C++(你在这里) | 第 4 篇:你的第一个 React Native JSI 函数 | 第 5 篇:HostObjects------把 C++ 类暴露给 JavaScript | 第 6 篇:内存所有权 | 第 7 篇:平台接线 | 第 8 篇:线程与异步 | 第 9 篇:实时音频管线 | 第 10 篇:存储引擎 | 第 11 篇:TurboModules vs Pure JSI vs Pure C++ | 第 12 篇:生产调试与陷阱

相关推荐
jxm_csdn15 小时前
Expo Go 本地命令行编译 apk(Ubutnu22.04)
react native
红尘散仙1 天前
一套 Rust 核心,跑通 Tauri + React Native
react native·react.js·rust
诚实可靠王大锤2 天前
React Native 输入框与按钮焦点冲突解决方案(rn版本0.70.3)
前端·javascript·react native·react.js
sealaugh325 天前
react native(学习笔记第四课) 英语打卡微应用(3)-ocr的文字转化成语音文件(tts)
笔记·学习·react native
wordbaby6 天前
如何封装一个生产级的 React Native 分页列表 Hook
前端·react native·react.js
沐言人生8 天前
ReactNative 源码分析5——ReactActivity之启动RN应用
android·react native
沐言人生9 天前
ReactNative 源码分析4——ReactActivity之加载JSBundle
android·react native
沐言人生11 天前
ReactNative 源码分析3——ReactActivity之初始化RN应用
android·react native
一个扣子11 天前
Hermes 未来路线图:2025 年起的新特性与 React Native New Architecture 协同
react native·未来发展·路线图·hermes·字节码diffing·性能增强
沐言人生11 天前
React Native 源码分析1——HybridData 机制深度分析
android·react native