把AI模型塞进App已经不算新鲜事了,语音识别、图像分类、实时滤镜、离线翻译,这些能力背后都离不开本地推理。对React Native开发者来说,问题在于JS和原生层之间隔着一座桥,模型推理恰恰是对数据传输最敏感的场景。一次推理可能要传几MB的图像数据,如果每次调用都要经过JSON序列化,性能损耗会非常直观地体现在帧率上。本文围绕JSI和Native Modules两种方式展开,从原理到代码逐层拆解。

一、Native Modules桥接:传统方式的原理与局限
Native Modules是React Native最经典的通信方案。JS线程通过NativeModules对象调用原生方法,消息会经过Bridge(桥接层)序列化成JSON字符串,再投递到Native线程执行,执行完毕后又把结果序列化回JS。这个设计在旧架构下工作得很好,但它的每一次调用都伴随着完整的序列化和反序列化过程。
对于普通业务接口,比如调用原生分享、获取设备信息,这种开销可以忽略。但AI推理不一样:输入张量可能是Shape为[1, 224, 224, 3]的浮点数组,包含超过15万个float元素。如果走JSON序列化,这些数字会先被转成字符串,跨线程后再解析回数字,单次传输的开销可能比推理本身还大。
先看一个典型的Native Modules实现,以Android端加载TensorFlow Lite为例:
public class TFLiteModule extends ReactContextBaseJavaModule {
private Interpreter interpreter;
TFLiteModule(ReactApplicationContext context) {
super(context);
}
@Override
public String getName() {
return "TFLiteModule";
}
@ReactMethod
public void loadModel(String modelPath, Promise promise) {
try {
Interpreter.Options options = new Interpreter.Options();
options.setNumThreads(4);
interpreter = new Interpreter(
new File(modelPath), options);
promise.resolve(true);
} catch (Exception e) {
promise.reject("LOAD_ERROR", e.getMessage());
}
}
@ReactMethod
public void runInference(ReadableArray input, Promise promise) {
// ReadableArray需要逐个读取元素,再组装成float数组
float[][][][] inputData = new float[1][224][224][3];
// 逐元素拷贝,这一步是性能瓶颈
for (int i = 0; i < input.size(); i++) {
// ...逐个填充张量数据
}
float[][] output = new float[1][1000];
interpreter.run(inputData, output);
WritableArray result = new WritableNativeArray();
for (float v : output[0]) {
result.pushDouble(v);
}
promise.resolve(result);
}
}
JS侧的调用代码很简单,但隐藏成本不小:
import { NativeModules } from 'react-native';
const { TFLiteModule } = NativeModules;
async function classify(imagePixels) {
await TFLiteModule.loadModel('mobilenet_v2.tflite');
const output = await TFLiteModule.runInference(imagePixels);
console.log('推理结果长度:', output.length);
}
这套流程的局限可以归纳为三点:一是数据必须复制多次(JS对象转JSON、JSON转Java数组、Java数组转ByteBuffer);二是异步通信,无法在JS线程同步拿到结果,做实时相机滤镜时每帧都要等待Promise回调;三是传输大数组时容易触发Bridge拥塞,导致JS线程卡顿。
二、JSI直连:绕过桥接的同步通信机制
JSI(JavaScript Interface)是React Native在0.58版本引入的C++抽象层,它允许C++代码直接持有JS对象的引用并调用其方法,反过来JS也可以直接调用C++暴露的HostObject。JSI的本质是一套与具体JS引擎无关的接口,无论底层是Hermes还是JSC,都通过同一套API操作JS值。
JSI最关键的能力是HostObject。你可以把它理解为一个C++对象被注入到JS全局作用域中,JS访问它的属性时,会直接触发C++函数调用,全程没有序列化,没有线程切换开销(如果实现得当),也没有异步等待。这意味着传一个TypedArray给C++,C++拿到的是指向同一块内存的指针,数据零拷贝。
下面是一个通过JSI暴露同步推理接口的C++实现骨架:
#include <jsi/jsi.h>
#include <vector>
using namespace facebook;
class TFLiteHostObject : public jsi::HostObject {
public:
// JS调用 tensor.runInference(inputArray) 时进入这里
jsi::Value get(jsi::Runtime &rt, const jsi::PropNameID &name) override {
auto methodName = name.utf8(rt);
if (methodName == "runInference") {
return jsi::Function::createFromHostFunction(
rt, name, 1,
[this](jsi::Runtime &rt,
const jsi::Value &,
const jsi::Value *args,
size_t count) -> jsi::Value {
// 直接从TypedArray读取底层缓冲区,零拷贝
auto &arrayBuffer =
args[0].asObject(rt).getPropertyAsObject(
rt, "buffer");
auto arrayData =
arrayBuffer.getAsArrayBuffer(rt);
const float *input =
reinterpret_cast<const float *>(
arrayData.data(rt));
std::vector<float> output(1000);
interpreter->Invoke(
input, output.data());
// 构造返回的TypedArray,同样零拷贝路径
jsi::Function arrayCtor = rt.global()
.getPropertyAsFunction(rt, "Float32Array");
jsi::Object result =
arrayCtor.callAsConstructor(rt, 1000)
.asObject(rt);
// 写入输出数据...
return result;
});
}
return jsi::Value::undefined();
}
private:
tflite::Interpreter *interpreter = nullptr;
};
// 安装到全局,一般在RN初始化时调用
void install(jsi::Runtime &rt) {
auto hostObj = std::make_shared<TFLiteHostObject>();
rt.global().setProperty(
rt, "nativeTFLite",
jsi::Object::createFromHostObject(rt, hostObj));
}
JS侧的使用体验发生了质变——同步调用,直接访问全局对象:
// 无需Promise,无异步等待 const output = nativeTFLite.runInference(inputFloat32Array); const maxIndex = output.indexOf(Math.max(...output));
需要注意线程问题。JSI的HostObject默认在JS线程被调用,而推理是CPU密集操作,直接在JS线程执行会阻塞UI。工程上的做法是在C++侧维护一个推理线程队列,HostObject内部把数据指针投递到工作线程,如果业务允许同步等待就join,否则可以配合Promise风格封装。此外,在Android上需要通过Package的getCxxBases或在合适的生命周期钩子里执行install,iOS则在初始化Bridge时注入,这部分脚手架工作可以借助react-native-builder-bob之类的工具简化。
三、方案对比与选型建议
两条路线没有绝对优劣,关键看场景。下面从几个维度做对比:
| 对比维度 | Native Modules桥接 | JSI |
|---|---|---|
| 通信方式 | 异步消息,JSON序列化 | 同步直调,零拷贝 |
| 数据传输成本 | 大数组开销显著,多次复制 | TypedArray共享底层内存 |
| 调用延迟 | 毫秒级,受Bridge负载影响 | 微秒级,稳定可预测 |
| 开发门槛 | 低,Java/Objective-C即可 | 高,需要C++与构建系统知识 |
| 调试便利性 | Chrome DevTools直接可用 | 断点调试需切换环境 |
如果只是调用一次模型做图像分类,或者用Core ML处理单张照片,Native Modules完全够用,开发成本低、维护简单,团队里不需要懂C++的人。但如果是相机实时滤镜、逐帧目标检测、流式语音识别这类高频推理场景,JSI的优势会非常明显——实测中,一张224x224的图像张量经桥接传输大约要额外消耗十几毫秒,而JSI路径几乎可以忽略,帧率差距直接体现在体验上。
还有一点值得留意:React Native新架构下的Turbo Modules本身就是构建在JSI之上的,旧的Native Modules会被逐步兼容替代。也就是说,即使现在选择走桥接方案,随着项目升级到新架构,底层通信机制也会自然向JSI靠拢。提前熟悉JSI的编程模型,对长期维护是有价值的。
落地时的建议很朴素:先用Native Modules快速验证模型效果,确认推理链路可行;再评估调用频率和数据规模,如果性能成为瓶颈,把推理接口这一小部分用JSI重写,其余业务模块保持原样。渐进式迁移比推翻重来务实得多。
React NativeJSINative Modules修改时间:2026-09-03 14:55:25