阅读视图

发现新文章,点击刷新页面。

【翻译】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 函数长什么样:

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)

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

栈上分配:自动生命周期

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

JavaScript 类比是函数内部 let 的生命周期:它在函数执行期间存在,之后变为可被垃圾回收。
但关键差异是:C++ 栈对象销毁是立即且确定的。它不会等“将来某次 GC 轮到它”,而是在右花括号处就发生。每一次都如此,且有语言层面的保证。

堆(Heap)

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

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

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

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

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

心智模型:

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

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

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


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

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

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

function addItem(list) {
  list.push("new item");
}

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

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

按值传递(Copy)

按值传递:产生副本

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

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

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

按引用传递(&

按引用传递:原数据别名

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

类型后面的 & 表示“这不是副本,而是同一份数据的另一个名字”。最接近的 JavaScript 类比是把对象传进函数:函数能改对象属性,因为它拿到的是同一份数据的引用。
但 C++ 引用还更进一步:如果你在函数里给引用参数重新赋值(例如 text = "new value"),会直接改调用方变量本体;而 JavaScript 里在函数内部 param = newValue 不会影响调用方。

常量引用(const &

常量引用:只读别名

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

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

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

指针(*

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

指针:地址操作语义

int value = 42;
int* ptr = &value;
std::cout << *ptr;    // 42

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

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 常见手动清理:

function readFile(path) {
  const handle = openFile(path);
  try {
    return handle.read();
  } finally {
    handle.close();
  }
}

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

在 C++ 里,RAII 的含义是:构造函数负责获取资源,析构函数负责释放资源。
由于对象离开作用域时(包括栈展开过程)析构函数会自动运行,所以清理是有保证的——即使抛出异常也一样。

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

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++ 析构函数会运行,并清理对应的原生资源。

心智模型:

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(转移所有权)。

#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

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

#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”释放)。

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 篇预告)

// 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 闭包

function makeCounter() {
  let count = 0;
  return () => ++count;
}

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

auto makeCounter() {
    int count = 0;
    return [count]() mutable { return ++count; };
}

语法看起来不一样,但可观察结果相同:连续调用返回函数 3 次,你会得到 1、2、3。
内部机制不同——JS 捕获的是变量绑定(同作用域闭包可共享),而 C\+\+ 的 [count] mutable 是捕获私有副本——但在“返回单个计数器”这个场景下结果一致。

语法骨架:

[capture](parameters) -> return_type { body }

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

捕获模式

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

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:对象是共享的,不是拷贝的

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

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

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

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:不拷贝,直接转移

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 = 直接把文档递给别人,几乎瞬时,但你自己不再持有原件。

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

// 把 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 模块:所有核心概念都在里面

#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 里这样调用:

storage.set("theme", "dark");
const theme = storage.get("theme");
console.log(theme);

输出:

"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++ 认知,能从这些符号里直接看出来。

当前系统视图:

 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 模式

{
    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 篇:生产调试与陷阱

❌