文章

原生模块开发实践深度解析

从Native Module注册到Turbo Module,完整拆解原生模块的模块注册、方法导出、线程策略和类型映射四大核心环节。

原生模块开发实践深度解析

一句话概括

RN 原生模块的价值在于「突破框架天花板」——当内置 API 不够用时,自己写原生代码扩展;核心掌握四件事:模块怎么注册、方法怎么暴露、线程在哪里跑、数据怎么映射。

核心知识点

1. 模块注册流程——旧架构 vs 新架构

旧架构的核心是一个「收集 → 注册 → 初始化」的三步走:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Step 1: 实现模块
public class ToastModule extends ReactContextBaseJavaModule {
    @Override public String getName() { return "Toast"; }  // JS侧用 NativeModules.Toast 访问

    @ReactMethod
    public void show(String message, int duration, Promise promise) {
        Toast.makeText(getReactApplicationContext(), message, duration).show();
        promise.resolve(true);
    }
}

// Step 2: 打包进 Package
public class ToastPackage implements ReactPackage {
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext ctx) {
        return Arrays.asList(new ToastModule(ctx));
    }
}

// Step 3: 在 MainApplication 注册
@Override
protected List<ReactPackage> getPackages() {
    return Arrays.asList(new MainReactPackage(), new ToastPackage());
}

新架构(Turbo Module)砍掉了手动注册——你写 TypeScript 接口,Codegen 生成 C++ 桩代码,JSI 运行时自动绑定:

1
2
3
4
// 只需定义接口,Codegen 自动生成原生代码框架
export interface Spec extends TurboModule {
  show(message: string, duration: number): Promise<boolean>;
}

关键区别: 旧架构「运行时反射」,新架构「编译期代码生成」——从毫秒级变成微秒级。

2. 方法导出规范——Promise 是标准答案

1
2
3
4
5
6
7
8
9
10
11
@ReactMethod
public void fetchUser(String userId, Promise promise) {
    try {
        User user = database.query(userId);
        // ✅ resolve 回 JS 侧 → await 拿到结果
        promise.resolve(serializeUser(user));
    } catch (Exception e) {
        // ✅ reject → JS 侧 try-catch 捕获
        promise.reject("FETCH_ERROR", e.getMessage(), e);
    }
}

为什么 Promise 比 Callback 好?

 PromiseCallback
错误处理try-catch 一把梭每个回调都要手动判断 error
async/await原生支持需要自己包装
Codegen 支持一等公民不是
多次回调❌ 不行✅ 可以(上传进度、传感器数据流)

结论: 默认用 Promise,只有「需要多次回传数据」时才用 Callback(或用事件发射器 DeviceEventEmitter 替代)。

3. 线程指定策略——别在主线程做 I/O

默认情况下 @ReactMethod 方法跑在原生模块线程(独立于 UI 线程和 JS 线程),但这不代表可以随便写:

1
2
3
4
5
6
7
8
9
10
11
12
@ReactMethod
public void readLargeFile(String path, Promise promise) {
    // ✅ 默认在 Module 线程,可以直接做 I/O
    byte[] data = Files.readAllBytes(Paths.get(path));

    // ⚠️ 但更新 UI 必须切到主线程
    getReactApplicationContext().runOnUiQueueThread(() -> {
        textView.setText(new String(data));
    });

    promise.resolve(data.length);
}

三条铁律:

  1. I/O、加解密、图像处理 → 原生模块线程(默认就在这,直接做)
  2. View 更新 → runOnUiQueueThread() 切到主线程
  3. 同步方法(isBlockingSynchronousMethod = true)→ 必须在 1ms 内完成,否则卡 JS 线程。只能用 int/boolean/String 返回值

4. 类型映射表——JS 和原生的数据「翻译」

写原生模块时最常查的就是这张表:

JSAndroid JavaiOS ObjC
stringStringNSString
numberDouble / intNSNumber
booleanBooleanBOOL
ArrayReadableArrayNSArray
ObjectReadableMapNSDictionary
FunctionCallbackRCTResponseSenderBlock
PromisePromiseRCTPromiseResolveBlock / RCTPromiseRejectBlock

读写复杂对象时注意防御性检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
@ReactMethod
public void processConfig(ReadableMap config, Promise promise) {
    // ✅ 先 hasKey,避免 MissingKeyException
    String theme = config.hasKey("theme") ? config.getString("theme") : "light";
    int timeout = config.hasKey("timeout") ? config.getInt("timeout") : 5000;

    // 嵌套对象
    if (config.hasKey("advanced")) {
        ReadableMap advanced = config.getMap("advanced");
        // ...
    }
    promise.resolve(/* result */);
}

5. 资源管理——别留下悬空引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class SensorModule extends ReactContextBaseJavaModule implements LifecycleEventListener {
    private SensorManager sensorManager;
    private SensorEventListener listener;

    @ReactMethod
    public void startListening(Promise promise) {
        sensorManager = (SensorManager) getReactApplicationContext()
            .getSystemService(Context.SENSOR_SERVICE);
        listener = event -> { /* 处理传感器事件 */ };
        sensorManager.registerListener(listener,
            sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
            SensorManager.SENSOR_DELAY_NORMAL);
        promise.resolve(true);
    }

    // ⚠️ 关键:清理资源
    @ReactMethod
    public void stopListening(Promise promise) {
        if (sensorManager != null && listener != null) {
            sensorManager.unregisterListener(listener);
            listener = null;
        }
        promise.resolve(true);
    }

    // 兜底:Activity 销毁时自动清理
    @Override
    public void onHostDestroy() {
        stopListening(null);
    }
}

忘记注销传感器监听 = 内存泄漏 + 持续耗电。 始终实现 LifecycleEventListener 或在模块的 onCatalystInstanceDestroy() 中回收资源。

其实你每天都在用

  • 扫码 App 调用摄像头: 底层就是原生模块——react-native-camera 封装了 Android 的 Camera2 API 和 iOS 的 AVFoundation
  • App 内自动填充验证码: 原生模块监听短信广播(Android SMS_RECEIVED),读到验证码通过 Bridge 回传给 JS 填入输入框
  • 分享到微信/微博: 原生模块调微信 SDK / 微博 SDK,因为这些都是原生 .jar/.framework
  • 蓝牙连接设备: Web 有 Web Bluetooth API,但 RN 里蓝牙通信必须写原生模块(BluetoothGatt / CBCentralManager)
  • App 打开时判断网络类型(WiFi/4G/5G): @react-native-community/netinfo 底层就是原生模块在调系统的 ConnectivityManager / SCNetworkReachability

常见误解(FAQ)

❌ 误区1:「@ReactMethod 的方法自动跑在子线程,所以直接在里面更新 UI 也行」

原生模块方法默认跑在 Module 线程(非 UI 线程),直接操作 View 会抛 CalledFromWrongThreadException。更新 UI 必须通过 runOnUiQueueThread() 显式切线程。

❌ 误区2:「同步方法(isBlockingSynchronousMethod)很方便,多用」

同步方法会阻塞 JS 线程直到原生侧执行完成。如果原生侧花了 10ms,JS 线程就卡 10ms——动画直接掉一帧。只用于读缓存状态、设备信息等 <1ms 的操作。而且 iOS 的 App Store 审核可能拒绝频繁使用同步 JNI 调用的应用。

❌ 误区3:「原生模块开发只能在 Java / ObjC 里写,不能跨平台复用」

你可以把核心逻辑写在 C++ 里(#ifdef ANDROID / #ifdef __APPLE__),Android 通过 JNI 调用,iOS 通过 ObjC++ 调用,实现三端代码复用。Turbo Module 原生就是 C++ 优先的。

❌ 误区4:「新架构上线了,旧模块直接全重写」

不需要。新架构提供了 Interop Layer——旧模块可以不做任何修改就在新架构下运行(通过 RCT_NEW_ARCH_ENABLED 条件编译)。策略是:新模块用 Turbo Module,旧模块渐进替换。

一句话总结

原生模块是 RN 的「逃生舱」——框架做不到的事你可以自己捅破天花板;记住四条铁律:默认用 Promise、耗时操作在后台线程、更新 UI 切主线程、用完的资源要注销。

本文由作者按照 CC BY 4.0 进行授权