在 React Native 的世界里,JavaScript 就像是总部的指挥官,而 Android 和 iOS 则是两个驻扎在不同国家的本地团队。总部下达命令用统一的“总部语言”(JavaScript),但两个团队真正干活时,却需要用各自的“本地语言”(Java/Kotlin 和 Swift)。原生模块开发,就是为这种跨国协作编写的一本“标准化操作手册”——它规定总部如何下达指令、本地团队如何执行并汇报结果、遇到紧急情况如何主动向总部报警。当你掌握了这本手册的编写方法,你就能让 React Native 应用拥有调用系统相机、访问通讯录、读取传感器等等“本地人”才有的超能力。
对 AI 从业者而言,掌握原生模块开发意味着你不仅能训练和部署模型,还能将 AI 能力无缝嵌入移动应用,调用设备端原生性能实现端侧智能——这让你的 AI 解决方案从“能用”升级为“好用”。
可视化演示
展示React Native桥接架构三层通信、线程模型任务分发与事件发射Promise回调机制,支持模式切换。
本节配有交互动效(进入视口后自动加载,约 3KB)。
RN桥接架构 JS线程通过Bridge向原生线程发送异步消息 Bridge通过JSON序列化传输模块名+方法名+参数 NativeModules.Mod.method(args) RN桥接实现JS与Android/iOS原生能力异步通信
原生模块概览
想象你是一家跨国公司的总部协调员。公司有两个本地办公室:一个在柏林(Android),一个在东京(iOS)。总部用的官方语言是 JavaScript,但柏林办公室内部用 Java/Kotlin 交流,东京办公室用 Swift/Objective-C 交流。总部想让他们执行某个任务(比如“读取手机电池电量”),不能直接对柏林同事说德语——总部不会说德语。这时候,就需要一本“协作手册”来规定:总部用英文下达标准的任务单,柏林办公室用德语执行后,把结果翻译成英文交回总部。在 React Native 的世界里,这本“协作手册”就是原生模块(Native Module)。
原生模块解决的正是 React Native 的能力边界问题。React Native 的 JavaScript 层能处理 UI 逻辑、业务状态,但遇到操作系统级别的功能——调用摄像头、读取通讯录、访问传感器——就鞭长莫及了。原因是 JavaScript 运行在受限的 JavaScriptCore(或 Hermes)引擎中,无法直接访问操作系统 API。原生模块就像在总部指挥中心和本地团队之间建立的一条专属热线,把原生的“本地能力”以 JavaScript 函数的形式暴露给 RN 代码调用。
原生模块的开发思路是双平台各自实现、JS 侧统一调用。也就是说,Android 团队用 Kotlin 写一套逻辑,iOS 团队用 Swift 写一套逻辑,两套逻辑完成同样的“任务”,但都在各自的办公室里用各自的语言干活。对于 JS 总部的指挥官来说,他只需要记住一个统一的 JavaScript API 名字,不需要关心底层是 Android 还是 iOS。这种“一个接口、双端实现”的模式,正是跨平台开发的精髓——总部只认手册上的标准流程,柏林和东京各自用自己的方式完成。
Android实现
让我们把镜头切到柏林的 Android 办公室。总部发来了一份任务单:“请实现一个模块,能够获取设备型号和电池电量。”柏林团队需要做的是:用他们的母语 Kotlin(或 Java)创建一个“本地团队”——准确地说,是一个继承自 ReactContextBaseJavaModule 的类。这个基类就像是柏林办公室的统一制服,穿上它,团队成员才能被总部识别。
第一步是重写 getName() 方法,这个方法返回一个字符串,就是这本协作手册里“柏林团队”的代号。总部将来调用时,就用这个名字来定位到柏林团队。比如返回 "DeviceInfo",那么 JS 侧通过 NativeModules.DeviceInfo 就能找到它。这就像办公室门口的铭牌,让总部派来的信使一眼就能找到正确的门。
第二步是声明具体的方法,并用 @ReactMethod 注解标记。这个注解就是盖章——表示“这个方法可以接受总部的远程指令”。没有盖这个章的方法,即使写在类里,总部也无权调用。看一个典型例子:
需要注意的是参数类型。JS 侧传递过来的参数,在 Android 端被映射为标准的 Java/Kotlin 类型:Boolean、Double、String、ReadableMap(对应 JS 对象)、ReadableArray(对应 JS 数组)。如果你在 JS 侧传入了一个数字数组,在 Kotlin 侧收到的就是一个 ReadableArray。这就是手册里的“翻译规则”——总部语言中的每个词,柏林团队都规定了对应的德语词,不能随便翻译。
class DeviceInfoModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { override fun getName() = "DeviceInfo" @ReactMethod fun getBatteryLevel(promise: Promise) { // 获取电池电量的原生代码 val level = getBatteryLevelFromSystem() if (level != -1) { promise.resolve(level) } else { promise.reject("ERROR", "无法读取电量") } }}
iOS实现
镜头转向东京的 iOS 办公室。东京团队接到的是同一份任务单,但他们用 Swift 来完成。在 iOS 的世界里,一个原生模块需要遵循 RCTBridgeModule 协议——这相当于东京办公室的“员工身份认证”。任何想要接收总部指令的类,都必须先签署这份协议,表明“我愿意遵守总部协作手册的规则”。
Swift 中实现原生模块的关键在于暴露给 Objective-C 运行时。由于 React Native 的桥接层是 Objective-C 写的,Swift 类需要通过 @objc 关键字将自己的方法和属性暴露给 Objective-C 运行时查看。类还要提供一个返回模块名的静态方法 moduleName(),这个方法的返回值就是 iOS 侧注册到总部的模块代号。这类似于柏林办公室的 getName()。
方法的导出使用 @objc 加 RCT_EXTERN_METHODS 宏(在 Objective-C 中)或在 Swift 中通过 @objc 标记方法并用 RCT_EXPORT_METHOD 宏导出。在 Swift 中通常这样写:
在 iOS 中,方法的回调方式有两种:一种是使用 RCTResponseSenderBlock(传统的回调函数方式),另一种是使用 RCTPromiseResolveBlock 和 RCTPromiseRejectBlock(Promise 方式)。现代 RN 开发中,Promise 方式更推荐,因为它在 JS 侧天然支持 async/await 语法,代码更简洁易读。
iOS 端的注意点是 Swift 与 Objective-C 的互操作。React Native 的模块系统底层用 Objective-C 的运行时动态查找方法,所以 Swift 中每一个要导出给 JS 的方法都必须标记 @objc,且方法签名必须符合桥接层的要求。如果方法签名写错了,桥接层找不到对应的方法,JS 侧调用就会直接报错——就像东京团队的门牌号写错了,总部的信使在楼里转了一圈也找不到人。
@objc(DeviceInfoModule)class DeviceInfoModule: NSObject, RCTBridgeModule { static func moduleName() -> String! { return "DeviceInfo" } @objc func getBatteryLevel(_ resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) { let level = UIDevice.current.batteryLevel if level >= 0 { resolve(level) } else { reject(NSError(domain: "DeviceInfo", code: -1, userInfo: [NSLocalizedDescriptionKey: "无法读取电量"])) } }}
注册与导出
柏林和东京的团队都建设好了,但总部还需要一本完整的“通讯录”——所有团队的门牌号、专长领域、可接任务的清单。这就是模块的注册(Registration)环节。没有完成注册的模块,即使类写得再完美,总部也调用不到——就像团队没在总部人力系统备案,总部根本不知道有这个团队存在。
Android 端的注册流程是这样的:需要创建一个 ReactPackage 的实现类,在 createNativeModules() 方法中返回你的模块实例列表,然后用 addPackage() 方法把这个包添加到 MainApplication 的 getPackages() 列表中。代码看起来像这样:
iOS 端的注册相对简单。在 Objective-C 中,使用 RCT_EXPORT_MODULE() 宏就能完成注册;如果用 Swift,则通过 RCTBridgeModule 协议 + moduleName() 方法自动完成。iOS 还有一种导出控件的方法,但那是 View 相关的内容,我们这里只关注模块方法。
导出方法时需要注意的是参数类型的一一对应。JS 侧的方法参数与原生方法参数必须按顺序匹配——如果 JS 侧调用 getBatteryLevel()(无参数),但原生侧定义了 getBatteryLevel(promise: Promise),桥接层仍然能找到方法,因为 Promise 是最后自动注入的;但如果 JS 侧传入 (string, number),原生侧期望 (Double, String),类型不匹配就会导致调用失败。这就好比总部发来的任务单上写着“用德语回复”,但柏林团队用日语回复——两边都懵了。
还有一个常见陷阱:Android 中导出方法时不要使用 Boolean 以外的 Java 包装类作为简单参数类型(如 Integer、Float),桥接层对基本类型映射最稳定。同时,方法的返回值必须为 void,结果必须通过回调或 Promise 返回——JS 和原生之间是异步通信,原生方法无法同步返回结果。这就像总部下达任务后不能站在门口等结果,而是先回办公室,等柏林团队完成后通过信使把结果送回总部。
class DeviceInfoPackage : ReactPackage { override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> = listOf(DeviceInfoModule(reactContext)) override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()}
线程与回调
现在,总部发出了调用指令,柏林团队和东京团队开始行动了。这时就涉及一个关键问题:任务在哪个“工作台”上执行? React Native 的线程模型可以用一个三层办公室结构来理解:最顶层是 JS 线程(总部办公室),中间层是原生模块线程池(各团队的公共工作区),底层是 UI 主线程(团队的“客户接待室”)。
默认情况下,Android 的原生模块方法会在一个独立的原生线程池中执行,iOS 的方法会在桥接层分配的队列中执行——它们都不在主线程上。这样做的好处是:耗时操作(比如文件读取、网络请求)不会阻塞 UI。但坏处是:如果你在原生方法中直接操作 UI(比如弹一个 Toast 或者更新 View),就必须切换到主线程。Android 上使用 reactContext.runOnUiQueueThread(),iOS 上使用 DispatchQueue.main.async。
线程管理中最核心的概念是Promise 回调。总部(JS 侧)发出调用后不会干等着,而是留下一个“回执”——Promise。原生侧完成计算后,通过调用 promise.resolve(result) 或 promise.reject(error) 来回传结果。这个回传是异步的,总部收到后继续执行后续逻辑。JS 侧这样调用:
回调结果传递的过程中,需要注意两点。第一,回调可能从任意线程发出(执行任务的线程);第二,JS 侧收到回调后会回到 JS 线程继续执行,这个过程由桥接层自动处理。但如果你在原生侧把一个耗时操作放到了后台线程,然后在这个后台线程里调用 resolve,这个 Promise 的“回传信使”会自动把结果带到 JS 线程——你不需要手动处理线程切换。
一个常见的问题是:在 Android 的原生方法中,如果你在后台线程执行完任务后,还想弹出 UI 提示(比如 Toast),就必须回到 UI 线程。RN 提供了专门的 API,Android 端可以直接使用 reactContext.getCurrentActivity()?.runOnUiThread { ... }。iOS 端在后台队列执行完任务后,如果要在主线程更新 UI,也需要手动切换到主队列。记住这个原则:耗时操作后台跑,UI 操作主线程,回调通过 Promise 异步送达。
const level = await NativeModules.DeviceInfo.getBatteryLevel();console.log('电池电量', level);
事件发射机制
前面讲的都是总部主动打电话给本地团队,那如果本地团队想主动向总部报告消息呢?比如电池电量下降到了 20%,或者用户截了一张图——这些是原生侧主动发生的事件,JS 总部并不知情。这时候就需要事件发射机制(Events)。这就好像东京办公室的传感器监测到了台风,需要主动向总部发警报——不需要总部先来问“今天天气怎么样”。
在 Android 端,事件发射通过 reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) 获取事件发射器,然后调用 emit(eventName, params) 发送事件。在 iOS 端则通过 self.bridge.eventDispatcher() 或 self sendEvent(withName:body:) 发送。两端的事件名必须一致,这样 JS 侧监听时才能用同一个名字。
JS 侧的监听方式如下:
这里最大的坑是生命周期管理。如果你在组件中注册了事件监听,但在组件卸载时没有移除监听,就会造成内存泄漏——JS 侧还保留着对原生事件的引用,原生侧反复发射事件,导致无用的回调不断触发。正确做法是:在 useEffect 的清理函数中调用 subscription.remove(),或者用 emitter.removeAllListeners('onBatteryLow')。
事件发射的另一个要点是跨平台统一。Android 的事件发射器和 iOS 的事件发射器在 JS 侧都归结为同一个 NativeEventEmitter API,你只需要在 JS 侧创建一个 emitter 实例,然后监听特定的事件名即可。事件名在两端的原生代码中必须完全一致——如果 Android 端发的是 "onBatteryLow",iOS 端发的也是 "onBatteryLow",JS 侧就能用同一个监听器处理两个平台的事件。这就好比柏林和东京虽然用不同的语言汇报,但汇报的标题栏都统一用英文填写,总部才能统一处理。
import { NativeEventEmitter, NativeModules } from 'react-native'; const { DeviceInfo } = NativeModules;const emitter = new NativeEventEmitter(DeviceInfo); const subscription = emitter.addListener('onBatteryLow', (data) => { console.log('电量低!', data.level);}); // 不再需要时,记得移除监听subscription.remove();
优化与内存管理
最后,我们来聊聊让协作手册更高效的优化技巧。原生模块的每一次 JS 与原生之间的通信,都要经过桥接层的序列化、传输、反序列化——就像总部和本地团队之间每次通信都要派一个信使,信使来回跑是要耗费时间和体力的。如果总部每隔 100 毫秒就派一个信使去问“电池电量是多少”,不仅信使累垮,连总部和本地团队的工作效率都会下降。
第一项优化:减少桥接调用频率。 将频繁的小调用合并为一次大的调用。比如游戏应用中需要监听陀螺仪的连续数据,如果每帧都通过桥接传一次数据,性能会很差;更好的做法是原生侧积累 10 帧的数据后一次传给 JS。就像总部不要求柏林团队“每完成一个小任务就汇报一次”,而是“每完成一个阶段再统一汇报”。
第二项优化:合理管理监听器资源。 原生侧在 onCatalystInstanceDestroy(Android)或 invalidate(iOS)时,要清理所有注册的监听器和事件发射器。Android 端有专门的 onHostDestroy 回调,可以在 Activity 销毁时释放资源。这就像办公室下班时要关灯断电,不然一晚上电都白白浪费。
第三项优化:注意大对象和引用的释放。 不要让原生侧持有 JS 侧传递的大对象(如 ReadableMap、ReadableArray)的引用,因为 JS 侧的对象在原生侧引用后,JS 的垃圾回收无法回收它,会导致内存泄漏。正确的做法是:用完后立即置空引用,或者只提取需要的字段后丢弃原始对象。另外,Promise 回调对象不要长期持有——一旦 resolve 或 reject 之后,应该立刻释放引用,否则回调闭包持有的上下文不会被回收。
内存问题最典型的场景是:在原生模块中启动了一个长期运行的后台任务(比如位置监听),任务持有模块实例的引用,模块又持有 ReactContext 的引用,导致整个 Activity 无法被回收。这种问题在 Android 上尤其常见。解决办法是在模块的 invalidate() 方法中停止任务、清除监听器、释放引用。
AI 应用场景
场景一:端侧 AI 推理的相机模块封装
阶段:模型部署与推理
解决的问题:RN 应用需要调用系统相机进行实时画面采集,并将帧数据送入端侧推理引擎(如 TensorFlow Lite)。传统做法是把图像数据通过 Base64 编码后从 JS 传到原生侧,每次传输几十 KB,延迟高且占内存。通过原生模块,可以直接在原生侧捕获相机帧,将灰度转换、缩放等预处理放在原生栈中完成,只将最终推理结果(如检测框坐标、分类标签)通过 Promise 回传给 JS。
具体案例:一个 AI 拍照翻译 App,相机预览用原生模块驱动,图片预处理(裁剪、旋转、亮度调整)全部在原生侧完成,推理结果通过事件发射机制实时推送识别文本到 RN 界面。JS 侧只负责渲染结果和交互逻辑。
场景二:AI 语音助手的录音与音频流模块
阶段:数据采集与预处理
解决的问题:语音助手需要持续采集麦克风音频流,并将音频帧实时送入门控或唤醒词检测模型。RN 的 JS 层无法直接操作麦克风,原生模块实现音频流采集,并将音频分帧后通过事件机制高频推送给 JS 侧(或原生侧直接跑模型)。这样避免每次录音都经过网络,实现离线语音唤醒。
具体案例:一个跨平台的语音笔记 App,通过原生模块在后台持续监听麦克风,检测到“你好小助”的唤醒词后触发录音,录音文件先缓存在原生侧,再进行 VAD(语音活动检测),最后将有效语音段上传服务端识别。全程用户无感知,续航表现良好。
场景三:AI 健康检测的传感器数据桥接
阶段:多模态数据融合
解决的问题:健康监测 App 需要读取心率、血氧、加速度计等多传感器数据,并实时分析。不同传感器的数据率不同(心率 1Hz、加速度计 50Hz),如果全部通过 JS 桥接传输,会造成大量线程切换开销。原生模块先在原生侧对多路传感数据进行时间戳对齐和轻度滤波,再以压缩后的特征帧一次性传给 JS 侧做上层逻辑判断。
具体案例:一个跑步健身 App 同时采集心率与加速度数据,原生模块负责数据对齐与运动状态初判(跑步/走路/静止),通过事件机制每 500ms 推送一次状态向量到 JS 侧,JS 侧仅负责 UI 展示和异常提醒(如心率过高)。
Python 演示
import jsonimport threadingimport timefrom collections import defaultdict class Promise: """模拟 RN 的 Promise 回调对象""" def __init__(self): self._resolve = None self._reject = None def then(self, resolve, reject=None): self._resolve = resolve self._reject = reject return self def resolve(self, value): if self._resolve: self._resolve(value) def reject(self, error): if self._reject: self._reject(error) class NativeModuleBase: """模拟原生模块基类(类似 ReactContextBaseJavaModule)""" module_name = "BaseModule" def getName(self): return self.module_name class AndroidDeviceModule(NativeModuleBase): """模拟 Android 端原生模块(Kotlin 实现)""" module_name = "DeviceInfo" def __init__(self): self._listeners = [] def getBatteryLevel(self, promise): # 模拟异步获取电量 def _task(): time.sleep(0.1) promise.resolve(85) threading.Thread(target=_task).start() def startBatteryMonitor(self, event_emitter): # 模拟持续发射事件 def _monitor(): for i in range(3): time.sleep(0.5) event_emitter.emit("onBatteryChange", {"level": 85 - i * 5}) threading.Thread(target=_monitor).start() class IOSDeviceModule(NativeModuleBase): """模拟 iOS 端原生模块(Swift 实现)""" module_name = "DeviceInfo" def getBatteryLevel(self, promise): promise.resolve(88) class Bridge: """模拟桥接层:模块注册 + 方法分发 + 事件发射""" def __init__(self): self._modules = {} self._event_listeners = defaultdict(list) self._platform_impl = {} def registerModule(self, module, platform): self._modules[module.getName()] = module self._platform_impl[module.getName()] = platform def call(self, module_name, method_name, *args): module = self._modules.get(module_name) if not module: raise RuntimeError(f"模块 {module_name} 未注册") fn = getattr(module, method_name, None) if not fn: raise RuntimeError(f"方法 {method_name} 不存在") promise = Promise() last_arg = args[-1] if args else None if isinstance(last_arg, Promise): fn(*args[:-1], promise) else: fn(promise) return promise def emit(self, event_name, payload): for cb in self._event_listeners[event_name]: cb(payload) def addListener(self, event_name, callback): self._event_listeners[event_name].append(callback) def removeListeners(self, event_name): self._event_listeners[event_name].clear() def main(): bridge = Bridge() # 按平台注册模块(双平台同界面,同逻辑) bridge.registerModule(AndroidDeviceModule(), "android") bridge.registerModule(IOSDeviceModule(), "ios") # JS 侧调用获取电量(走 Android 实现) def on_resolve(value): print(f"[JS] 收到电量: {value}%") promise = bridge.call("DeviceInfo", "getBatteryLevel") promise.then(on_resolve) # JS 侧监听电池变化事件 bridge.addListener("onBatteryChange", lambda data: print(f"[JS] 事件: 电量变为 {data['level']}%")) # 启动 Android 端的持续事件发射 android_module = bridge._modules["DeviceInfo"] android_module.startBatteryMonitor(bridge) # 让异步任务完成 time.sleep(2) print("[JS] 移除事件监听") bridge.removeListeners("onBatteryChange") if __name__ == "__main__": main()
promise.resolve(85)模拟原生侧异步任务完成后,通过 Promise 将结果回传给 JS 侧。bridge.registerModule(AndroidDeviceModule(), "android")模拟按平台注册原生模块,实现“一个接口、双端实现”。bridge.emit("onBatteryChange", {"level": ...})模拟原生侧主动发事件,JS 侧通过监听器接收。module.getName()模拟原生模块返回名称,供桥接层索引调用。event_emitter.emit()对应 RN 中RCTDeviceEventEmitter的事件发射机制。bridge.addListener()对应 JS 侧NativeEventEmitter.addListener()注册监听。getattr(module, method_name, None)模拟桥接层的动态方法查找与分发。bridge.removeListeners()模拟组件卸载时移除监听,防止内存泄漏。
常见误区
误区“React Native 是跨平台的,所以原生模块也应该是跨平台的,我只需要写一份代码,两个平台都能用。”
正解原生模块的本质是“接口统一、实现分离”。JS 侧的调用代码可以完全一致,但 Android 必须用 Java/Kotlin 实现,iOS 必须用 Swift/Objective-C 实现。就像柏林办公室和东京办公室都执行总部的“读取电量”任务,但柏林用德语读系统服务,东京用日语读 UIDevice。你不能把 Swift 的代码直接搬到 Kotlin 里运行。
为何常见:“跨平台”这个词容易让人误以为连原生层也是共享的。实际上 React Native 跨平台的是 JS 层和 UI 层,原生能力仍然需要双端各自实现。
误区“Promise 的 resolve 方法在任何线程调用都没问题,反正 JS 侧能收到就行。”
正解Promise 回调可以在任意线程发起,但发起后的数据序列化和 JS 线程调度由桥接层自动处理。问题在于,如果你在后台线程中执行完任务后,又想在原生侧更新 UI,不能直接在那个线程里操作——UI 操作必须回到主线程。Android 用 runOnUiQueueThread,iOS 用 DispatchQueue.main.async。Promise 只负责回传数据,不负责帮你切线程做 UI。
为何常见:初学者以为 Promise 回调就是“线程安全的万能通道”,忽视了原生侧 UI 线程模型的存在。
误区“我注册了事件监听,React Native 会在组件销毁时自动帮我清理,不需要手动移除。”
正解JS 侧的 NativeEventEmitter.addListener() 返回一个订阅对象,必须手动调用 subscription.remove() 或在 useEffect 清理函数中移除监听。如果组件卸载后监听器仍然存在,原生侧每次发射事件都会触发回调,导致内存泄漏和逻辑错误。Android 端的 onHostDestroy 和 iOS 端的 invalidate 也要求你在原生侧主动清理资源。
为何常见:很多开发者习惯了 Web 前端框架的自动清理机制,忽视了 RN 桥接层中 JS 与原生之间的引用关系是隐式且长期存活的。
误区“反正桥接层是异步的,我可以每帧都向原生侧发送数据,React Native 会处理好的。”
正解每一次桥接调用都有序列化、跨线程传输、反序列化的固定开销。高频调用(如 60fps 的实时数据)会占用大量桥接带宽,导致 UI 卡顿。正确的做法是批量传输:原生侧积累一段时间的数据后一次性发给 JS,或者通过 addListener 的 batch 参数做批量事件传递。
为何常见:JS 和原生之间的通信看起来像普通函数调用,实际上底层是跨线程的异步消息传递,性能开销被“异步”的表象所掩盖。
误区“原生方法返回一个自定义对象,桥接层会自动帮我序列化成 JS 对象,JS 侧直接访问属性就行。”
正解桥接层只能序列化标准的 JSON 可序列化数据类型。Android 上自定义 Kotlin data class 不能直接返回给 JS,必须手动转换成 WritableMap;iOS 上自定义 Swift 结构体也需要转换成 NSDictionary 或兼容的 JSON 对象。JavaScript 侧的 NativeModules 调用只能返回桥接层认识的类型,自定义类型会静默失败或抛出异常。
为何常见:现代编程语言的对象序列化机制让开发者习惯了自动序列化,但 RN 桥接层的序列化能力是有限且固定的,不具备通用反射序列化能力。
面试题
Q1. React Native 中原生模块的 Android 端实现,需要继承哪个基类?
- A.
ReactContextBaseJavaModule← 正确答案 - B.
ReactActivity - C.
NativeModuleBase - D.
BaseReactModule
答案:A。Android 端原生模块必须继承 ReactContextBaseJavaModule,它提供了与 ReactContext 交互的基础能力。ReactActivity 是 Activity 的基类,与原生模块无关;NativeModuleBase 和 BaseReactModule 在 RN 的 Android 端并不存在,是干扰项。
Q2. iOS 端(Swift)实现原生模块时,需要遵循哪个协议?
- A.
RCTBridgeModule← 正确答案 - B.
NSObjectProtocol - C.
RNNativeBridge - D.
SwiftModuleDelegate
答案:A。iOS 端(Swift 或 Objective-C)实现原生模块需要遵循 RCTBridgeModule 协议,它是 RN 桥接层识别原生模块的标准协议。NSObjectProtocol 是所有 NSObject 都遵循的基础协议,不提供模块能力;另外两个选项是杜撰的名字。
Q3. 原生模块的方法需要返回数据给 JS 侧时,正确的做法是什么?
- A.直接 return 一个字符串,桥接层自动处理
- B.使用 Promise 或 Callback 异步回传结果← 正确答案
- C.通过全局变量传递,JS 侧自行读取
- D.将结果写入文件,JS 侧读文件
答案:B。RN 桥接是异步的,原生方法必须通过 Promise(或 Callback)回传结果,不能同步 return。直接 return 在桥接层会被忽略;全局变量和文件读写都绕过了桥接机制,不是合法的通信方式。
Q4. 在 Android 的原生模块中,如果需要弹出一个 Toast,正确的线程处理方式是?
- A.直接在模块方法中调用 Toast,无需切线程
- B.在 ReactContext 的 runOnUiQueueThread 中执行 Toast← 正确答案
- C.在后台线程池中执行 Toast
- D.通过 Promise 回传给 JS 后由 JS 触发
答案:B。UI 操作(如 Toast)必须在主线程执行。原生产模块方法默认运行在原生线程池中,需要切换到 UI 线程。直接弹 Toast 可能在子线程崩溃;后台线程池执行仍是子线程;回传给 JS 再触发 Toast 绕了远路,且无法直接控制原生 UI。
Q5. iOS 端导出方法回传结果时,Swift 中应该使用什么类型的回调参数?
- A.
(String) -> Void - B.
RCTPromiseResolveBlock与RCTPromiseRejectBlock← 正确答案 - C.
NSObject -> Void - D.
@escaping (Bool) -> Void
答案:B。iOS 桥接层提供的 Promise 回调类型是 RCTPromiseResolveBlock 和 RCTPromiseRejectBlock,它们会被映射为 JS 侧的 Promise。普通的 Swift 闭包无法被桥接层识别;NSObject -> Void 和 (Bool) -> Void 不是 RN 桥接的标准回调类型。
Q6. 以下哪项是 Android 端注册原生模块的必需步骤?
- A.实现 ReactPackage 并加入 MainApplication 的包列表← 正确答案
- B.在 AndroidManifest.xml 中声明模块
- C.在 build.gradle 中添加模块依赖
- D.创建一个新的 Activity 继承模块类
答案:A。Android 端注册原生模块必须通过 ReactPackage 实现类,并在 MainApplication.getPackages() 中添加。AndroidManifest.xml 用来声明权限和组件,不需要声明原生模块;build.gradle 只管依赖配置;Activity 继承模块类完全不相关。
Q7. 关于 JS 侧调用原生模块方法的参数类型,以下哪些说法是正确的?
- A.Android 端 JS 对象对应 ReadableMap← 正确答案
- B.Android 端 JS 数组对应 ReadableArray← 正确答案
- C.iOS 端 JS 数字对应 NSInteger
- D.两端方法参数的顺序可以随意调整
答案:AB。Android 端 JS 对象和数组分别对应 ReadableMap 和 ReadableArray,这是桥接层定义的映射。iOS 端 JS 数字映射的是 NSNumber 而不是 NSInteger(后者是原生 C 类型);参数的顺序必须与 JS 调用一致,随意调整会导致类型不匹配。
Q8. 关于原生模块的事件发射(Events),以下哪些做法是正确的?
- A.在组件卸载时移除事件监听器← 正确答案
- B.Android 端通过 DeviceEventManagerModule 的 emit 发送事件← 正确答案
- C.事件名在 Android 和 iOS 两端可以不同
- D.使用 NativeEventEmitter 接收事件← 正确答案
答案:ABD。正确的做法包括:组件卸载时移除监听器防止泄漏;Android 端通过 DeviceEventManagerModule.RCTDeviceEventEmitter 发射事件;JS 侧用 NativeEventEmitter 接收。事件名在 Android 和 iOS 两端必须一致,否则 JS 侧无法用统一的名字监听。
Q9. 原生模块的优化中,处理大量高频数据(如传感器数据)的最佳实践是?
- A.每次数据变化都通过桥接层直接传给 JS
- B.在原生侧聚合数据,降低桥接传输频率← 正确答案
- C.将数据写入 SQLite,JS 侧查询
- D.用全局变量存储数据,JS 定时读取
答案:B。高频数据的最佳实践是原生侧聚合(比如累积多个采样点后一次性传输),以降低桥接调用的开销。每次都传会导致桥接层拥堵、UI 卡顿;写数据库和全局变量绕过了桥接机制,引入额外延迟和不必要的复杂度。
Q10. 以下哪些做法能有效防止原生模块导致的内存泄漏?
- A.在模块销毁时移除所有事件监听器← 正确答案
- B.长时间持有 Promise 对象的引用
- C.后台任务完成后释放 ReactContext 引用← 正确答案
- D.长期持有 JS 传入的大型 ReadableMap
答案:AC。防止内存泄漏的正确做法包括:模块销毁时移除监听器、后台任务完成后释放 ReactContext 的引用。长时间持有 Promise 会阻止回调闭包被回收,大型 ReadableMap 长期持有会导致 JS 侧对象无法被 GC 回收——这两者都是常见的泄漏原因,选它们反而是错误做法。