原生模块开发实践深度解析
从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 好?
| Promise | Callback | |
|---|---|---|
| 错误处理 | 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);
}
三条铁律:
- I/O、加解密、图像处理 → 原生模块线程(默认就在这,直接做)
- View 更新 →
runOnUiQueueThread()切到主线程 - 同步方法(
isBlockingSynchronousMethod = true)→ 必须在 1ms 内完成,否则卡 JS 线程。只能用int/boolean/String返回值
4. 类型映射表——JS 和原生的数据「翻译」
写原生模块时最常查的就是这张表:
| JS | Android Java | iOS ObjC |
|---|---|---|
string | String | NSString |
number | Double / int | NSNumber |
boolean | Boolean | BOOL |
Array | ReadableArray | NSArray |
Object | ReadableMap | NSDictionary |
Function | Callback | RCTResponseSenderBlock |
Promise | Promise | RCTPromiseResolveBlock / 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 切主线程、用完的资源要注销。