Skip to Content
本人正在找工作,有合适的岗位可以联系我,简历
博客React Native Kotlin 原生模块桥接

React Native 与 Kotlin 原生模块桥接:人脸识别与 TTS 语音播报实现

在先知体检通项目中,单纯依赖 React Native 的 JS 层已经无法满足需求:人脸识别身份核验依赖厂商提供的 Android SDK(纯 Java/Kotlin API),语音播报引导需要调用系统级 TTS 服务并支持队列管理。这两个功能都需要通过编写 Kotlin 原生模块(Native Module)来实现。

这篇文章完整记录桥接过程中遇到的问题与解决方案。


1. 原生模块的基础结构

React Native 的 Native Module 机制要求:

  1. 在 Android 侧创建继承 ReactContextBaseJavaModule 的 Kotlin 类
  2. @ReactMethod 注解暴露给 JS 的方法
  3. 创建 ReactPackage 注册该模块
  4. JS 侧通过 NativeModules 访问

以 TTS 模块为例,先搭骨架:

// android/app/src/main/java/com/app/tts/TTSModule.kt package com.app.tts import android.speech.tts.TextToSpeech import com.facebook.react.bridge.* import com.facebook.react.modules.core.DeviceEventManagerModule import java.util.Locale import java.util.LinkedList class TTSModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { override fun getName() = "TTSModule" // JS 侧通过此名称访问 private var tts: TextToSpeech? = null private val speakQueue = LinkedList<String>() private var isInitialized = false }

2. TTS 语音播报模块:解决队列中断问题

2.1 初始化与队列管理

体检流程中,TTS 播报有严格的顺序要求:“请站上体重秤” → 等待测量完成 → “测量完成,请移步下一项”。如果 JS 侧快速连续调用 speak,默认的 QUEUE_FLUSH 模式会导致后一条打断前一条。

我设计了一个受控队列

@ReactMethod fun speak(text: String, priority: Int, promise: Promise) { if (!isInitialized) { promise.reject("TTS_NOT_READY", "TTS engine not initialized") return } if (priority == PRIORITY_HIGH) { // 高优先级:清空队列,立即播报(如紧急提示) speakQueue.clear() tts?.stop() executeSpeech(text, promise) } else { // 普通优先级:加入队列,等待上一条播完 speakQueue.offer(text) if (speakQueue.size == 1) { // 队列空时立即开始 processQueue(promise) } } } private fun processQueue(promise: Promise) { val text = speakQueue.peek() ?: return tts?.speak(text, TextToSpeech.QUEUE_FLUSH, null, generateUtteranceId()) }

2.2 监听播报完成事件

TTS 的关键是知道”何时播完”,从而触发下一步流程(如开始硬件测量)。通过 UtteranceProgressListener 回调 JS:

tts?.setOnUtteranceProgressListener(object : UtteranceProgressListener() { override fun onDone(utteranceId: String?) { speakQueue.poll() // 移除已播报的文本 // 通知 JS 侧当前语音完成 sendEvent("tts:done", Arguments.createMap().apply { putString("utteranceId", utteranceId) }) // 继续处理队列 if (speakQueue.isNotEmpty()) { processQueue(null) } } override fun onError(utteranceId: String?) { sendEvent("tts:error", Arguments.createMap().apply { putString("utteranceId", utteranceId) }) } // Android API 21+ 才有 onStart override fun onStart(utteranceId: String?) {} }) private fun sendEvent(eventName: String, params: WritableMap) { reactContext .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, params) }

2.3 JS 侧封装为 Hook

// hooks/useTTS.ts import { NativeModules, NativeEventEmitter } from 'react-native' const { TTSModule } = NativeModules const emitter = new NativeEventEmitter(TTSModule) export function useTTS() { const speak = useCallback((text: string, priority: 'normal' | 'high' = 'normal') => { return new Promise<void>((resolve, reject) => { TTSModule.speak(text, priority === 'high' ? 1 : 0) .then(resolve) .catch(reject) }) }, []) const onDone = useCallback((callback: () => void) => { const sub = emitter.addListener('tts:done', callback) return () => sub.remove() }, []) return { speak, onDone } }

3. 人脸识别模块:跨进程 Activity 结果回传

人脸识别是更复杂的场景,因为厂商 SDK 的调用方式是启动一个新的 Activity,识别完成后通过 onActivityResult 返回结果。这在 RN 的模块体系中需要特别处理。

3.1 注册 Activity 事件监听器

RN 的 BaseActivityEventListener 专门处理这类场景:

class FaceRecognitionModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext), ActivityEventListener { private var currentPromise: Promise? = null private val REQUEST_CODE_FACE = 1001 init { reactContext.addActivityEventListener(this) } override fun getName() = "FaceRecognitionModule" @ReactMethod fun startVerification(config: ReadableMap, promise: Promise) { currentPromise = promise val activity = currentActivity ?: run { promise.reject("NO_ACTIVITY", "Current activity is null") return } // 启动厂商人脸识别 Activity val intent = FaceSDK.buildIntent(activity).apply { putExtra("maxRetryCount", config.getInt("maxRetryCount")) putExtra("timeout", config.getInt("timeout")) } activity.startActivityForResult(intent, REQUEST_CODE_FACE) } override fun onActivityResult( activity: Activity?, requestCode: Int, resultCode: Int, data: Intent? ) { if (requestCode != REQUEST_CODE_FACE) return val promise = currentPromise ?: return currentPromise = null if (resultCode == Activity.RESULT_OK) { val userId = data?.getStringExtra("userId") ?: "" val score = data?.getFloatExtra("score", 0f) ?: 0f promise.resolve(Arguments.createMap().apply { putString("userId", userId) putDouble("score", score.toDouble()) putBoolean("success", score >= 0.85f) }) } else { promise.reject("FACE_CANCELLED", "User cancelled or verification failed") } } override fun onNewIntent(intent: Intent?) {} }

3.2 JS 侧的错误处理

// 业务层调用示例 async function verifyIdentity(userId: string) { try { const result = await NativeModules.FaceRecognitionModule.startVerification({ maxRetryCount: 3, timeout: 30000, }) if (!result.success) { // 相似度不足 0.85,提示用户 await tts.speak(`身份核验失败,相似度 ${Math.round(result.score * 100)}%,请重试`) return false } await tts.speak('身份核验成功,欢迎您') return true } catch (error) { if (error.code === 'FACE_CANCELLED') { await tts.speak('已取消核验') } else { await tts.speak('系统错误,请联系工作人员') console.error('[FaceRecognition]', error) } return false } }

4. 踩坑记录

坑 1:currentActivity 为 null 的时机
在 RN 应用刚启动或 Activity 重建时,currentActivity 可能为 null。原先没有做空检查导致 Promise 永远 pending,后来加了 null 检查并立即 reject,JS 侧会走 catch 流程给用户反馈。

坑 2:Promise 内存泄漏
如果用户直接按 Home 键退出了识别 Activity(不走 onActivityResult),currentPromise 会永远持有引用。解决方案是在 onHostDestroy(Activity 销毁回调)中主动 reject 并清空:

override fun onHostDestroy() { currentPromise?.reject("MODULE_DESTROYED", "Activity destroyed") currentPromise = null }

坑 3:TTS 在低端机上初始化延迟
部分体检终端是 Android 7 的老机器,TTS 引擎初始化需要 2-3 秒。如果初始化未完成就调用 speak 会直接失败。最终在应用启动时预热 TTS 引擎,并用一个 initPromise 让所有 speak 调用都等待初始化完成:

private var initPromise: Promise? = null private var isInitialized = false @ReactMethod fun init(promise: Promise) { initPromise = promise tts = TextToSpeech(reactContext) { status -> if (status == TextToSpeech.SUCCESS) { isInitialized = true tts?.language = Locale.CHINESE initPromise?.resolve(null) } else { initPromise?.reject("TTS_INIT_FAILED", "Status: $status") } initPromise = null } }

总结

编写 RN Native Module 的核心心得:

  1. 承诺(Promise)即契约:所有 @ReactMethod 带 Promise 的方法都必须有确定的 resolve/reject 路径,不能让它”悬空”。
  2. 生命周期对齐:Native Module 需要感知 Activity 生命周期,ActivityEventListenerLifecycleEventListener 是必备工具。
  3. 事件驱动优于轮询:像 TTS 完成、串口数据到达这类异步通知,用 DeviceEventEmitter 主动推送比 JS 侧轮询要稳定得多。

最后更新:2026年4月

标签:React Native、Kotlin、原生模块、人脸识别、TTS、Android开发

最近更新:4/19/2026, 3:19:53 PM