React 原生通信演进:探讨 JSI(JavaScript Interface)对 React 生态跨端调用链路的重构

React 原生通信演进:从“翻译官”到“黑客”的奇幻漂流

(聚光灯打在舞台中央,一位穿着格子衬衫、头发略显凌乱但眼神犀利的专家走上讲台。他手里没有拿激光笔,而是抓着一个旧手机。)

嘿,大家下午好!

我是你们的老朋友。今天我们不聊怎么写 useState,也不聊怎么把那个丑陋的 Alert.alert 换成自定义的 Modal。今天我们要聊的是 React Native 的“幕后黑手”,是那个让 JS 层和 Native 层像谈恋爱一样又像吵架一样纠缠不清的东西——通信机制

如果 React Native 是一个跨国婚姻,那么 JavaScript 和 Objective-C/Swift/Kotlin 之间的通信就是那个在中间牵线搭桥的“翻译官”。以前,这个翻译官是个只会说“你好”和“再见”的文盲,效率极低。而现在,我们换掉了他,换成了一个懂编程、懂内存、甚至懂黑客技术的“黑科技”。

这个黑科技,就是 JSI (JavaScript Interface)

来,把你们的笔记本电脑翻过来,准备迎接一场从 2015 年开始的穿越之旅。


第一章:那个只会说 JSON 的“翻译官”

在 React Native 早期(也就是我们常说的“老架构”),JS 和 Native 之间的对话是这样的:

场景一:JS 传参数给 Native
JS 写了一封信:“嘿,哥们,帮我调个相机,参数是 { width: 1080, height: 1920 }。”
JS 运行时把这个 JSON 字符串扔给桥。
桥接层(Bridge)接过来,把它解析成 C++ 字符串,找到对应的 Native 方法(比如 openCamera),把参数塞进去,调用原生方法。
原生方法执行完,把结果包成 JSON 字符串,再走一遍流程传回 JS。
JS 收到字符串,JSON.parse 一下,得到结果。

场景二:Native 传事件给 JS
Native 发生了一个震动事件。
它得把这个震动事件打包成 JSON,扔给桥。
桥把这个 JSON 传给 JS 运行时(JSI 或 V8)。
JS 运行时解析 JSON,找到对应的监听器,执行回调函数。

你看,这个过程像不像一个快递员
你(JS)写好快递单(JSON),快递员(Bridge)骑着自行车(序列化/反序列化)送到仓库(Native),仓库处理完,再写快递单,再骑车回来。
而且,这个快递员有个致命的缺点:他不认识你的东西。他只认识快递单号(字符串)。每次他都要把你的东西拆开、压扁、再重新装回去。

代码示例:老架构的悲剧

// JS 端
import { NativeModules } from 'react-native';

// 我们想获取设备信息
const getDeviceInfo = async () => {
  try {
    // 这里的 processDeviceData 是一个耗时操作吗?不,仅仅是解析 JSON
    const rawInfo = await NativeModules.DeviceInfo.getDeviceDetails(); 
    // 假设返回的是 JSON 字符串: "{"model":"iPhone 14", "battery": 80}"
    const deviceInfo = JSON.parse(rawInfo);

    // 现在我们有了数据,可以开始干活了
    console.log(`Model: ${deviceInfo.model}`);
  } catch (e) {
    console.error(e);
  }
};
// Native 端 (Objective-C)
@implementation RCT_EXTERN_MODULE(DeviceInfo, NSObject)

RCT_EXTERN_METHOD(getDeviceDetails:(RCTPromiseResolveBlock)resolve
                  reject:(RCTPromiseRejectBlock)reject)
{
  // 1. 准备数据
  NSDictionary *info = @{
    @"model": [[UIDevice currentDevice] model],
    @"battery": @([[UIDevice currentDevice] batteryLevel] * 100)
  };

  // 2. 转换为 JSON 字符串 (序列化!这是 CPU 密集型操作!)
  NSError *error;
  NSData *jsonData = [NSJSONSerialization dataWithJSONObject:info options:0 error:&error];
  NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];

  // 3. 回调给 JS
  if (error) {
    reject(@"serialization_error", @"Failed to serialize", error);
  } else {
    resolve(jsonString); // 像扔垃圾一样把字符串扔回去
  }
}

@end

痛点分析:

  1. 序列化开销JSON.stringifyJSON.parse 是 CPU 密集型操作。如果你传一个巨大的数组,或者每秒传 100 次,JS 线程会卡顿。
  2. 内存分配:每次调用都要分配新的字符串内存,垃圾回收器(GC)会哭死。
  3. 异步延迟:虽然 Promise 是异步的,但桥接层的调度本身有延迟。
  4. 类型丢失:JSON 只支持 Number, String, Boolean, Array, Object。你不能传一个 Date 对象,不能传一个 Function,不能传一个 Buffer。你只能传字符串,传完还得解析。

第二章:JSI 的诞生——当翻译官变成了黑客

Facebook 工程师们受不了了。他们想:“我们为什么要用文本格式(JSON)来传递二进制数据?我们为什么不能直接把内存指针传过去?”

于是,JSI (JavaScript Interface) 诞生了。

JSI 不是一个新的 JavaScript 引擎,它是 Facebook 为 JS 引擎(V8, Hermes, JSC)提供的一套 C++ API
简单来说,JSI 允许 C++ 代码直接操作 JavaScript 的运行时环境。

  • 以前:JS 运行时(V8)不知道 C++ 的存在,只能通过桥接层用字符串交流。
  • 现在:C++ 代码直接注册函数给 JS 运行时。JS 运行时可以直接调用 C++ 函数。C++ 也可以直接读取 JS 对象的内存,甚至直接把 C++ 的对象传给 JS(通过 HostObject)。

JSI 的核心哲学:直接内存访问。

想象一下,以前你寄包裹(JSON),现在你直接把数据放在桌子上,两个人面对面交流。


第三章:TurboModules——JSI 的最佳拍档

光有 JSI 还不够,因为 React Native 还要兼容旧代码,还要处理复杂的依赖注入。于是,Facebook 引入了 TurboModules

TurboModules 是一种接口定义。它允许你用 TypeScript/Java/Kotlin 定义一个接口,然后通过代码生成工具,自动生成:

  1. C++ 的实现代码。
  2. JS 的包装代码。

这就像是你定义了一个“合同”,编译器帮你把合同翻译成双方都能听懂的语言。

代码示例:TurboModule 的定义与实现

1. 定义接口 (TypeScript / Flow)

// NativeMyModule.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  // 普通方法
  add(a: number, b: number): number;

  // 返回 Promise 的方法
  fetchData(): Promise<string>;

  // 接收回调的方法
  subscribeToEvents(onEvent: (data: string) => void): void;
}

export default TurboModuleRegistry.getEnforcing<Spec>('NativeMyModule');

2. C++ 实现 (C++)

这是 JSI 发挥威力的地方。注意,这里没有 JSON.stringify,没有 NSString 转换。

#include <jsi/jsi.h>
#include <react/renderer/turbo-modules/TurboModule.h>
#include <jsi/instrumentation.h>

using namespace facebook::jsi;

// 继承 TurboModule
class NativeMyModuleSpec : public facebook::react::TurboModule {
public:
  NativeMyModuleSpec(const JSIEnvironment& env, std::shared_ptr<CallInvoker> jsInvoker)
    : TurboModule(env, jsInvoker) {}

  // 1. 直接计算,返回数字
  Value add(Runtime& runtime, const Value& thisValue, const Value* args, size_t count) {
    if (count < 2) {
      throw JSError(runtime, "add requires 2 arguments");
    }
    double a = args[0].getNumber();
    double b = args[1].getNumber();
    return Value(a + b);
  }

  // 2. 直接返回字符串,不用 JSON!
  Value fetchData(Runtime& runtime, const Value& thisValue, const Value* args, size_t count) {
    // 模拟异步操作(这里简化为同步,实际可以用 Promise)
    std::string data = "This data comes from C++ directly, no JSON parsing needed!";
    return String::createFromUtf8(runtime, data);
  }

  // 3. 接收回调,直接调用!
  Value subscribeToEvents(Runtime& runtime, const Value& thisValue, const Value* args, size_t count) {
    if (count < 1 || !args[0].isObject()) {
      throw JSError(runtime, "Callback must be provided");
    }

    // 获取 JS 传入的函数对象
    Object callbackObj = args[0].asObject(runtime);
    Function callback = callbackObj.asFunction(runtime);

    // ... 在这里启动原生监听器 ...
    // 假设我们收到了一个震动事件
    // 直接调用 JS 函数,传参是 Value 类型,不是字符串!
    // 注意:如果是在主线程调用 JS,可能会阻塞 UI,通常需要线程池

    // 模拟回调
    // callback.call(runtime, "震动事件触发!");

    return Value::undefined();
  }
};

// 导出给 JSI 使用
std::shared_ptr<TurboModule> createNativeMyModule(
    const std::string& moduleName,
    const ObjCTurboModule::InitParams& params) {
  return std::make_shared<NativeMyModuleSpec>(params.env, params.callInvoker);
}

对比之前的代码,你看到了什么?

  1. 类型安全:C++ 知道传入的是 double,JS 端也定义的是 number。不需要类型转换。
  2. 性能add 方法是零开销的。没有字符串拷贝。
  3. 灵活性subscribeToEvents 直接传递了 JS 函数对象,而不是把函数名字符串传过去。这意味着原生可以直接执行 JS 代码,而不需要 JS 引擎去解析字符串。

第四章:HostObject——JSI 的黑魔法

如果说 TurboModule 是调用 C++ 的功能,那么 HostObject 就是让 C++ 拥有 JS 对象的属性。

这在 React Native 的动画库(如 Reanimated)中起到了关键作用。

场景:你想在 C++ 层维护一个复杂的数组状态,并在 JS 层实时更新它,但不想每次都序列化/反序列化。

代码示例:HostObject 的实现

#include <jsi/jsi.h>
#include <vector>
#include <string>

class MyHostObject : public facebook::jsi::HostObject {
public:
  MyHostObject(jsi::Runtime& runtime) {
    // 初始化一些数据
    data.push_back("Hello");
    data.push_back("World");
  }

  // 当 JS 试图访问 obj.myProp 时调用
  jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& name) {
    std::string key = name.utf8(runtime);

    if (key == "length") {
      return jsi::Value(static_cast<double>(data.size()));
    }

    if (key == "data") {
      // 返回一个 JS 数组
      jsi::Array array = jsi::Array(runtime, data.size());
      for (size_t i = 0; i < data.size(); i++) {
        array.setValueAtIndex(runtime, i, jsi::String::createFromUtf8(runtime, data[i]));
      }
      return array;
    }

    return jsi::Value::undefined();
  }

  // 当 JS 试图修改 obj.myProp 时调用
  void set(jsi::Runtime& runtime, const jsi::PropNameID& name, const jsi::Value& value) {
    std::string key = name.utf8(runtime);
    if (key == "push") {
      if (value.isString()) {
        std::string str = value.asString(runtime).utf8(runtime);
        data.push_back(str);
      }
    }
  }

private:
  std::vector<std::string> data;
};

JS 端调用:

// 在 JS 端,你不需要任何特殊的 import
// 你可以直接创建一个 HostObject 的实例(通常由原生层提供)
const nativeObj = new NativeHostObject(); // 假设这是通过 JSI 注入的

// 直接访问属性,没有任何 JSON 序列化过程
console.log(nativeObj.data); // 输出: ["Hello", "World"]
console.log(nativeObj.length); // 输出: 2

// 直接修改属性
nativeObj.data.push("React Native"); 
// C++ 层的 data 数组瞬间就变了!

这有多快?
这就像你在 Python 里直接操作 C++ 的内存对象一样快。没有序列化延迟,没有垃圾回收压力。这就是 Reanimated 能够实现 60fps+ 动画的核心原因——它把动画状态直接托管在 C++ 内存中,JS 只是指针引用,更新时直接写内存。


第五章:重构调用链路——从“异步”到“并发”

JSI 的引入,不仅仅是快了一点点,它从根本上改变了 React Native 的架构,促成了 Fabric 的诞生。

旧架构(基于 Bridge):

  1. JS 事件 -> Bridge -> Native -> Bridge -> JS 回调。
  2. 这是一条串行的链路。JS 在等 Native,Native 在等 JS。
  3. 主线程(UI 线程)经常被 Bridge 的序列化工作阻塞。

新架构(基于 JSI + TurboModules + Fabric):

  1. JS 事件 -> JSI -> C++ 调度器 -> Native。
  2. JSI 提供了直接调用的能力。
  3. 结合 Concurrent Features(并发特性),JS 运行时现在可以在执行 JS 代码的同时,C++ 层可以直接调用 JS 函数。

代码示例:并发通信

假设你正在做一个视频播放器。

旧方式(阻塞):

// JS 线程
function onVideoFrameReady() {
  // 这里的代码会阻塞 UI,因为要等 Bridge
  updateUI(); 
}

新方式(JSI + 虚拟化):
C++ 层直接把帧数据通过 JSI 抛给 JS 运行时的一个并发任务
JS 运行时收到数据,直接调用 JS 函数,不经过主线程,也不经过 Bridge 的序列化队列。

// C++ 层
void VideoPlayer::onFrameDecoded(const uint8_t* buffer, size_t size) {
  // 通过 JSI 直接调用 JS 的渲染函数
  // 这一步是异步的,且非常快
  jsiRuntime_.global().getPropertyAsFunction(jsiRuntime_, "renderFrame")
    .call(jsiRuntime_, jsi::Value::createFromBlob(jsiRuntime_, buffer, size));
}

这种架构允许 React Native 实现真正的并发。你可以在 JS 层开启多个“世界”(比如一个用于计算,一个用于渲染),它们之间通过 JSI 进行毫秒级的通信。


第六章:代码生成器——让 C++ 代码像写 TypeScript 一样爽

写纯 C++ 的 TurboModule 很强大,但也很繁琐。为了解决这个问题,React Native 引入了 Codegen

Codegen 是一个编译器。你只需要在 TypeScript 里写接口,Codegen 会自动生成:

  1. C++ 的 .cpp 文件(实现逻辑)。
  2. JS 的 .js 文件(类型安全的包装器)。

定义 (TypeScript)

// NativeButton.ts
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
  // 简单的按钮点击
  onPress(): void;

  // 带参数的
  setText(text: string): void;

  // 返回 Promise
  getBackgroundColor(): Promise<string>;
}

Codegen 自动生成的 C++ (简化版)

// NativeButton.cpp (自动生成)
#include "NativeButton.h"

// 1. 自动生成 JSI 调用逻辑
Value NativeButton_getBackgroundColor(Runtime& runtime, const Value& thisValue, const Value* args, size_t count) {
  // ... 调用原生逻辑 ...
  return String::createFromUtf8(runtime, "blue");
}

Value NativeButton_onPress(Runtime& runtime, const Value& thisValue, const Value* args, size_t count) {
  // ... 触发原生点击 ...
  return Value::undefined();
}

Value NativeButton_setText(Runtime& runtime, const Value& thisValue, const Value* args, size_t count) {
  // ... 设置文本 ...
  return Value::undefined();
}

Codegen 自动生成的 JS (简化版)

// NativeButton.js (自动生成)
import { TurboModuleRegistry } from 'react-native';
import type { Spec } from './NativeButton';

export default TurboModuleRegistry.getEnforcing<Spec>('NativeButton');

使用

import NativeButton from './NativeButton';

// TypeScript 会自动检查参数类型!
NativeButton.setText("Hello"); // ✅ 正确
NativeButton.setText(123);    // ❌ 编译错误!
NativeButton.setText(undefined); // ❌ 编译错误!

// 返回值也是强类型的
const color = await NativeButton.getBackgroundColor(); // string

这彻底解决了“桥接层”的类型安全问题。以前你传错参数,只有运行时才报错。现在,如果你在 C++ 里传了 int 而不是 string,Codegen 甚至不会生成那个方法。


第七章:内存管理——谁该负责清理垃圾?

在 JSI 时代,内存管理变得更加复杂,但也更加精细。

1. JSI Runtime 的生命周期
JSI Runtime(例如 Hermes 或 V8 的实例)是 C++ 对象。它通常由 Native 层持有。
当 React Native 销毁时,Native 层会销毁 JSI Runtime。这会导致 JS 的所有全局变量、函数、对象瞬间被销毁。

2. C++ 对象的持有
如果你在 C++ 里创建了一个对象,并通过 JSI 暴露给 JS,你需要确保 C++ 对象的生命周期长于 JS 的引用。
通常使用 std::shared_ptr 或者 jsi::Pointer 来管理。

3. JS 对象的引用
JSI 提供了 jsi::Runtime::makeHostObject。当 JS 不再引用这个对象时,JS 运行时会调用 HostObject 的析构函数。

示例:防止内存泄漏

class MyManager : public facebook::jsi::HostObject {
public:
  MyManager(jsi::Runtime& runtime) {
    // 创建一个 JS 数组并存储在 C++ 中
    jsi::Array arr = jsi::Array(runtime, 10);
    for (int i = 0; i < 10; i++) {
      arr.setValueAtIndex(runtime, i, jsi::Value(i));
    }
    // 将这个 JS 对象存储在 C++ 中
    // 注意:这里只是引用,JS 也在引用它
    myJsArray = std::make_unique<jsi::Array>(std::move(arr));
  }

  // 当 JS 不再使用这个对象时,这个函数会被调用
  ~MyManager() {
    // 清理 C++ 资源
    myJsArray.reset();
  }

  jsi::Value get(jsi::Runtime& runtime, const jsi::PropNameID& name) override {
    // ...
  }

private:
  std::unique_ptr<jsi::Array> myJsArray;
};

第八章:未来展望——WebAssembly 的潜在竞争者

聊了这么多 JSI,我们得谈谈未来。

JSI 目前主要绑定在 JavaScript 引擎上。虽然 JS 很灵活,但它的性能上限(尽管有 JIT)依然受限于解释器/编译器的机制。

WebAssembly (Wasm) 是一个字节码格式,性能接近原生。
React Native 团队正在探索 React Native with Wasm
如果未来我们能在 JSI 上直接加载 Wasm 模块,那么我们就能在 React Native 里运行 Rust、C++ 或 Go 编写的超高性能代码,而不需要经过 JavaScript 的中间层。

想象一下:

// Rust 代码
#[no_mangle]
pub extern "C" fn calculate_prime(num: u32) -> u32 {
    // 极致的计算性能
    // ...
}

// JSI 调用
let result = jsiRuntime.callFunction("calculate_prime", 1000000);

JSI 可能会成为连接 Wasm 和 JS 的桥梁。这将彻底重构 React Native 的“计算密集型”场景调用链路。


结语:不仅仅是性能,更是自由

我们回顾一下:

  1. 旧时代:JSON 桥接,慢、笨、容易出错。
  2. JSI 时代:直接内存访问,C++ 操作 JS 运行时,极速、灵活。
  3. TurboModules:通过代码生成,统一了接口,解决了类型安全。
  4. HostObject:让 C++ 对象像 JS 对象一样操作,打破了语言边界。
  5. 新架构:结合 Fabric,实现了真正的并发。

JSI 的出现,不仅仅是为了让 add(1, 1) 变得快那么简单。它打破了 JS 和 Native 之间的“墙”。现在,C++ 可以直接把数据塞给 JS,JS 可以直接把函数塞给 C++。

这就像是从“点外卖”(JS 调用 Native)进化到了“自己做饭”(直接通信)。

对于开发者来说,这意味着你可以编写更多的高性能原生模块,而不用担心拖慢整个应用的流畅度。对于架构师来说,这意味着你可以设计出更加解耦、更加高效的通信层。

所以,下次当你看到 React Native 代码里那个闪烁的加载圈,或者当你正在为一个大数组的数据传输而焦虑时,请记住那个躲在代码深处的“JSI 黑客”。它正在默默地把你的数据,以光速从 C++ 传到 JS,再传回 C++。

别客气,尽情利用它吧!

(专家收起手机,向观众挥手,灯光渐暗)

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注