先说个背景:我最近把手上一个 React Native 项目升级到了 SDK54 这条版本基线,顺手把工程里反复用到、反复踩坑的常用依赖重新过了一遍。很多刚接触 RN 原生开发的人,拿到官方模板第一反应是“这不就能跑了吗”,可真正接业务需求以后才发现,脚手架给的是最小可用集,导航、存储、设备信息、音频采集、动画这些东西,每一样都要自己往工程里补。这篇文章就是把我在 SDK54 上实际试过、跑通、也翻过车的依赖清单留个底,同时把安卓原生模块开发里不少人问过的回声消除需求拆开讲一遍。适合正在做 RN 原生开发、或者准备把老项目往新版本基线迁移的朋友参考,至少能少走点弯路。
我这里说的 SDK54,不是某个官方正式命名的 API Level,而是我手里这个 RN 原生开发工程的环境代号 / 版本基线。你如果在别处看到类似说法,大概率也是这个意思:React Native 版本、Android compileSdk、minSdk、targetSdk、Gradle 版本全部锁在一套组合里,后续装依赖才有共同语言。这套文章所有命令和配置,都默认你已经在 SDK54 这个基线上操作。
1. 先把这个项目的定位说清楚
1.1 SDK54 不是玄学,是版本基线
做原生开发的都知道,最怕的不是写代码,是环境不一致。SDK54 在我这里就是一套被验证过的组合:React Native 用了比较新的 0.7x 版本线,Android 侧 compileSdk 和 targetSdk 都固定到 35 左右,minSdk 放在 24 以上,Gradle 插件版本和 Kotlin 版本也在同一套兼容矩阵里。这么做的原因很简单:React Native 的依赖包对 RN 版本非常敏感,同一个包在 0.73 上没问题,升到 0.76 可能直接编译不过,或者 build 的时候告诉你某个 native 接口没了。
所以你在看任何依赖的安装文档之前,先把手头四个版本数字确认清楚:
| 项目 | 建议值 | 作用 |
|---|---|---|
| react-native | 锁定 0.7x 具体小版本 | 决定原生模块接口、新架构开关 |
| compileSdk | 33 ~ 35 | 决定你能引用哪些 Android SDK API |
| minSdk | 24 及以上 | 影响可用设备范围,也影响三方库要求 |
| targetSdk | 与应用商店要求同步 | 影响运行时权限、行为变更 |
这四个数一旦定了,依赖的版本选择就有据可依。比如某个需要原生代码的库,它的 README 通常会写“支持 RN 0.73+ / 新架构”,那它大概率也能跟你的 SDK54 基线兼容,但小版本差异仍然可能导致问题,所以我后面所有命令都强调一件事:看官方兼容表,别只看最新版。
1.2 为什么“常用依赖”要自己整理一份
官方模板为了保持最小可维护性,默认依赖少得可怜。你会看到里面只有 react、react-native、@react-native-community/cli 之类的基础包。可实际业务跑起来以后,你会发现自己需要的东西一大堆:页面跳转要 react-navigation,底部 Tab 要容器库,状态共享要 zustand 或 redux,本地缓存要 async-storage,获取设备型号要 device-info,图标要用 vector-icons,列表要 FlatList 但复杂手势又需要 gesture-handler,做动画则可能上 reanimated。
这些依赖如果临时想到哪个就装哪个,很容易出现两个问题。第一是版本互相打架,比如 react-native-screens 和 react-native-safe-area-context 的版本必须跟 react-navigation 主版本匹配,不然会出现找不到 native 方法或者路由白屏。第二是原生配置漏掉,有些库不是npm install就完事的,还要改 MainActivity、加 Babel plugin、res 目录放字体,漏一步就到运行时才报错。所以我把它们按场景整理成清单,目的就是让新成员加入项目时能照抄,而不是靠考古式排查。
2. 常用依赖分类清单:照着抄就行
2.1 导航容器:react-navigation 全家桶
导航是 RN 应用绕不开的组件,我目前的基线用的是@react-navigation/native配合@react-navigation/native-stack和@react-navigation/bottom-tabs。需要注意,这个库本身只是一个调度器,真正把原生页面容器和系统手势接进来,需要同时装它的三个搭档:
- react-native-screens:把页面切换下沉到原生层,减少内存占用和卡顿。
- react-native-safe-area-context:处理刘海屏、挖孔屏的安全区域。
- react-native-gesture-handler:让手势事件不走 JS 响应链,而是走原生手势识别器。
安装命令一般是:
npm install @react-navigation/native @react-navigation/native-stack @react-navigation/bottom-tabs npm install react-native-screens react-native-safe-area-context react-native-gesture-handler装完以后,iOS 侧要cd ios && pod install,Android 侧会自动链接。但有两个原生配置别忘:gesture-handler 要求在 MainActivity 的onCreate里调用GestureHandlerEnabled相关的初始化(不同版本写法不一样,以官方文档为准),screens 通常不需要额外初始化,但如果启动时白屏或返回栈异常,第一个怀疑对象就是它没配对版本。
我这个工程里把这三个的版本都锁在兼容矩阵里:react-navigation 主版本 7.x 时,screens 用 4.x,safe-area-context 用 5.x,gesture-handler 用 2.x。这几个数字是官方在升级文档里明确过的匹配范围,别贪新升大版本,除非你愿意顺手处理一次原生重构。
2.2 状态管理与数据请求:别都塞进 useState
业务复杂度一上来,光靠组件内 state 和 props 透传会把人逼疯。我这里的状态管理选型是 zustand,原因是它轻量、不需要 Provider 包裹、也没有模板代码,对 RN 环境非常友好。数据请求用的是 axios,因为它有拦截器、超时控制、取消请求这些现成能力,比裸 fetch 更适合真实项目。
npm install zustand axios如果你更习惯 redux 那一套,redux-toolkit + react-redux 也可以,但要注意安装量会大不少,而且要在入口处包一层 Provider。我个人在 RN 项目里偏向 zustand 的原因很简单:状态共享只需要create一个 store,然后在任意组件里useStore(...)就能取数,新团队成员上手成本非常低。
请求层我顺便封装了统一超时和错误码处理。一个常用的做法是写一个request.ts,把axios.create({ timeout: 10000 })放进去,再把 token 注入拦截器。这样整个项目里的请求路径保持一致,排查问题的时候只需要看一个文件。配合 zustand,可以把服务端状态和客户端临时状态分开:服务端数据走请求层,视图交互状态走 store。
2.3 设备能力与本地持久化:拿不到数据是常态
很多业务要读设备型号、系统版本、网络状态,这些 JS 侧拿不到,必须要原生库支持。我常用的是react-native-device-info和设备网络状态库@react-native-community/netinfo。前者能拿 DeviceId、系统版本、App 版本、唯一标识;后者能监听网络切换,断网时及时变 UI。
本地缓存方面,首选是@react-native-async-storage/async-storage。它是官方推荐的异步 key-value 存储,接口简单,适合存 token、用户偏好、启动配置。注意不要拿它存大对象或频繁写入的数据,它本质是序列化读写,性能上限很低,用来做登录态和小配置就够了。如果数据量上来,建议上react-native-sqlite-storage或op-sqlite,但这两个依赖需要原生编译配置,放在 SDK54 基线里要额外验证。
还有一个容易被忽略的包:react-native-localize。它做多语言本地化非常方便,能拿到系统地区、语言列表、时区,配合 i18n-js 或 react-i18next 可以做动态语言切换。没有它,你想做到“App 内切换语言且不用重启”基本是做梦,纯 JS 侧读不到系统 Locale 的完整信息。
2.4 UI 增强与动画方案:配置比安装更关键
RN 自带 Animated API 做简单动画没问题,但遇到手势联动、弹簧效果、复杂的页面转场,还是建议上react-native-reanimated。这个库在 SDK54 基线里要注意一点:新架构下要选 v4 或更高版本,并且要在 Babel 配置里加它的插件,不然一运行就报Reanimated 2 failed to create a worklet之类的问题。
{ "plugins": ["react-native-reanimated/plugin"] }图标这块,我目前还是用react-native-vector-icons,因为它字体资源丰富,引入方式也直观。不过它的安装步骤略烦:Android 要在android/app/build.gradle里配置apply from: "../../node_modules/react-native-vector-icons/fonts.gradle",iOS 要手动把需要的字体文件加入 Info.plist。如果不想折腾原生字体,可以看下@react-native-vector-icons系的新版方案,或者直接用 SVG 方案,这样就不用碰原生配置。
渐变、阴影这些视觉效果,我常用react-native-linear-gradient,它是一个成熟的老库,版本稳定。但注意在新架构下要确认它已经启用了 Fabric 支持,如果不用新架构反而无所谓。整体思路是:UI 依赖尽量少碰原生配置,越轻量越不容易在升级时爆炸。
3. 实操:从零初始化一个 SDK54 工程并跑通依赖
3.1 初始化命令与基础配置
如果你不是从老工程升级,而是想从零搭一个 SDK54 基线,最简单的方式是用社区 CLI 初始化:
npx @react-native-community/cli init RNDemoSDK54这个命令会生成带 RN 最新稳定版的工程。如果你想固定版本,可以这样:
npx @react-native-community/cli init RNDemoSDK54 --version 0.76.5我提醒一句:命令里的版本号必须精确到小版本,因为 RN 官方很激进,小版本之间也可能出现原生代码变化。初始化完成以后,先不要急着装依赖,要做的第一件事是锁定版本。把 package.json 里的 react 和 react-native 版本记下来,再去 npm 上看你要装的这些库各自的 peerDependencies。
比如某个库的 peerDependencies 写react-native >= 0.72.0,在 SDK54 基线上大概率兼容;如果写react-native >= 0.74.0, < 0.76.0,那你装之前就要慎重,很可能需要--legacy-peer-deps或换一个替代库。依赖版本冲突这个问题,越早发现越省事。
3.2 Android 侧原生配置清单
RN 工程跑起来以后,很多依赖的问题都出在 Android 原生这一层。我整理了一个自查清单,每次新装依赖都逐项过一遍:
- MainActivity 是否需要修改:比如 gesture-handler 在旧版本要求重写 onCreate,而 reanimated 有时需要改 getMainComponentName。
- Babel 插件是否添加:reanimated 的 worklet 插件漏配是白屏和启动报错的常见原因。
- 字体、so 库、资源文件是否复制:vector-icons 需要用 gradle 脚本导入字体,部分原生库需要把
.so放在指定目录。 - AndroidManifest 权限是否齐全:录音、网络状态、读取设备信息这些权限,JS 侧无法申请,必须在 AndroidManifest 声明。
用一个标准做法:依赖装完以后先跑一次 release 版构建,因为 debug 版很多时候会掩盖原生初始化问题,release 会严格检查资源打包和 ProGuard 混淆问题。在 SDK54 基线上,我常用:
cd android && ./gradlew assembleRelease这条命令能提前暴露很多Miss so库、font resource not found、duplicate class之类的问题,比在 debug 模式下点半天界面有效得多。
3.3 验证依赖是否正常:启动白屏问题
依赖装完、配置也加完以后,第一件事是跑起来看启动画面是否能正常跳到首页。很多人的项目在“依赖装好但页面白屏”这个状态卡住,其实大多是三类问题:
- react-native-screens 初始化失败:表现为首次路由无法渲染,页面长时间白板。
- reanimated 的 Babel plugin 没配:表现为启动时 console 有 worklet 相关报错,但界面也能出来,只是动画掉了。
- 原生库版本不匹配新架构:表现为 TurboModule 找不到,比如
NativeModule: X is null。
我建议用三分法排查:先看 Metro 日志有没有红色或黄色的报错,然后看 Android Logcat 里有没有ReactNativeJS开头的堆栈,最后用 adb 抓一个screencap看是原生层白屏还是 JS 层白屏。如果是原生层就还没走到 JS,重点查 MainActivity 和getMainComponentName;如果 Logcat 里有 JS 日志但界面不渲染,重点查路由和根组件。
4. 原生模块实战:把安卓回声消除封装给 RN 用
4.1 什么时候必须写原生代码
RN 的 JS 生态再丰富,也有到不了的地方,实时音频处理就是典型。你通过navigator.mediaDevices或 WebRTC 库能拿到音频流,但如果要做回声消除、降噪、自动增益这种低延时的音频处理,纯 JS 侧跑 DSP 算法基本不现实,性能和系统接入度都不够。
我手头有一个语音通话类需求,要求在安卓原生层开启回声消除,然后通过 RN 暴露出来的原生模块给 JS 调用。这个场景用到了 Android 提供的AcousticEchoCanceler,它属于android.media.audiofx包,可以对 AudioRecord 采集到的音频启作用。注意,Android 原生回声消除通常需要和设备硬件以及系统服务配合,不是所有设备都支持,所以封装时一定要先查isAvailable(),不支持的设备要降级策略。
4.2 AudioRecord 与 AcousticEchoCanceler 写法
下面是一段我实际的 Kotlin 核心代码,去掉业务包装,保留基本逻辑。首先要有一个录音会话,通常是AudioRecord,然后通过其AudioSessionId获取回声消除器实例。
@SuppressLint("MissingPermission") fun createEchoCanceler(audioRecord: AudioRecord): AcousticEchoCanceler? { return if (AcousticEchoCanceler.isAvailable()) { AcousticEchoCanceler.create(audioRecord.audioSessionId) } else { null } }拿到的AcousticEchoCanceler需要设置enabled = true才能真正生效。同时,为了让回声消除的效果听话,我一般会把模式设成setEchoCancelerMode或参考android.media.audiofx.AudioEffect的参数。但有一点要注意,回声消除器必须在 AudioRecord 开始录音之后启用,顺序反了会发现设置直接失败,用户那边听到的就是人在密室里的回声。
再补一个完整一点的录音配置示例:
val minBufferSize = AudioRecord.getMinBufferSize( 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT ) val audioRecord = AudioRecord( MediaRecorder.AudioSource.VOICE_COMMUNICATION, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, minBufferSize )使用VOICE_COMMUNICATION音源很重要,它本身就是为通话场景优化的,系统底层通常会增加一些自动增益或降噪处理,和回声消除器配合起来效果最好。如果你用MIC音源,它更偏向原声采集,不带那么多 DSP,回声消除效果会打折扣。
4.3 封装成 ReactNativeModule 并提供 JS 调用
原生逻辑跑通以后,下面要把这个能力暴露给 JS。这里要用到 RN 的ReactContextBaseJavaModule,在 SDK54 这条线上,如果开启新架构,还可以用 TurboModule 规范,但传统 NativeModule 也照常兼容。为了新人友好,我用传统写法。
class EchoCancellationModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { override fun getName() = "EchoCancellation" @RequiresPermission(Manifest.permission.RECORD_AUDIO) @ReactMethod fun isHardwareAecSupported(promise: Promise) { try { val available = AcousticEchoCanceler.isAvailable() promise.resolve(available) } catch (e: Exception) { promise.reject("AEC_CHECK_FAILED", e) } } }同时需要一个 Package 类把它注册进去:
class EchoCancellationPackage : ReactPackage { override fun createNativeModules(reactContext: ReactApplicationContext) = listOf(EchoCancellationModule(reactContext)) override fun createViewManagers(reactContext: ReactApplicationContext) = emptyList<ViewManager<*, *>>() }在 MainApplication 的getPackages()里把EchoCancellationPackage()加进去,JS 侧就能直接调用了:
import { NativeModules } from 'react-native'; const { EchoCancellation } = NativeModules; EchoCancellation.isHardwareAecSupported() .then((supported) => { if (supported) { // 开启原生采集处理流程 } }) .catch((err) => { // 降级处理 });这只是回声消除的第一步,真实场景里还需要把音频流回传给上层做编码或发送,那时候你就需要用到 AudioRecord 的循环读数据线程,并把这些 PCM 数据通过NativeEventEmitter发到 JS 侧,或者在原生层直接完成编码。但核心原则是一样的:原生层只做实时性强的部分,JS 侧只做业务编排。
5. 启动白屏与依赖冲突:我踩过的坑和排查顺序
5.1 React Native 启动白屏的常见原因
启动白屏是每个 RN 项目都会遇到的“老朋友”。我在项目里把它按发生阶段分成三类:
- 原生启动阶段白屏:日志还没出现
ReactNativeJS,一般是资源加载失败、MainActivity 找不到组件名、so 库缺失。 - JS 执行阶段白屏:日志有 JS 输出,但首页无法渲染,一般是路由初始化异常、根组件报错被吞、状态恢复失败。
- 新架构相关白屏:开启 Fabric 新架构以后,如果某些依赖没适配,会出现 native 组件无法挂载,具体表现为白屏 +
getViewManagerConfig报错。
每一种的排查路径不一样,但有一个共同技巧:打开 Metro 的日志过滤关键字ReactNativeJS,并且先用 debug 版本跑一遍,因为 debug 有红色报错框,比 release 下黑盒分析要直观得多。等 debug 过了再切 release,否则白屏会被混淆代码掩盖。
5.2 白屏排查与解决六步
我总结了一个固定的排查顺序,也被团队写进了项目文档。按顺序做,通常十分钟内能定位到问题:
- 检查 Metro 窗口是否有编译错误或模块解析错误,有则先解决 JS 层问题。
- 看 Android Logcat 里是否有
Unable to load script或ReactNativeJS: TypeError类的堆栈。 - 把路由入口简化成一个满屏
Text组件,确认是否所有页面白屏还是只有首页白屏,如果是只有首页白屏,重点看首页里用了哪个第三方组件。 - 逐个注释掉依赖库的调用,特别是导航容器和动画库,确认是哪个库在启动阶段拖垮了渲染。
- 清理缓存,包括
watchman watch-del-all、./gradlew clean、删除node_modules后重新安装。这个过程能解决很多“改了版本但没生效”的假白屏。 - 检查应用主题风格,RN 启动默认背景色是白色,如果 Application 主题里设置了透明背景或自定义样式,需要确保
windowBackground不为空。
第 6 点很容易被忽略。很多项目在原生 theme.xml 里设置了启动屏背景图,导致 RN 根视图渲染之前一直是白屏或空白图,这是预期的启动过渡,不算 bug。但如果启动图时间过长,就该优化 JS 初始化了,比如在原生层减少启动任务、延迟某些模块加载。
5.3 版本冲突与构建问题处理
依赖一多,Gradle 构建报错就成了家常便饭。最常见的是duplicate class和Failed to resolve configuration。前者通常是两个库打包了相同的 androidx 类文件,后者一般是某个依赖版本在 remote maven 上找不到对应 AAR。
我的习惯是先用./gradlew dependencies查看依赖树,找到冲突的传递依赖,再用resolutionStrategy强制指定统一版本。比如在android/app/build.gradle里:
configurations.all { resolutionStrategy { force "androidx.appcompat:appcompat:1.6.1" } }但要注意,force不是银弹,只能做最后手段,最优雅的办法是找到冲突的根,升级或降级直接的依赖包版本。还有一个经验:当某个 native 库升级以后发现打包体积异常增大,先去看它的 AAR 里是不是把so库拆成了armeabi-v7a/arm64-v8a/x86等,RN 默认会带上所有 ABI,如果你只发布真机,可以把abiFilters单独指定成arm64-v8a和armeabi-v7a,体积能小不少。
关于 React Native 新架构,我再提醒一句:SDK54 这个基线如果默认开启新架构,那么所有原生依赖都要确认自己有Fabric或TurboModule的实现文件。很多老库只做了旧架构兼容,新架构下运行当时不报错,一旦触发特定组件就会崩。最好的办法是升级前先看库的 GitHub Release Notes,里面通常会写“New Architecture support”。没有写的话,建议暂时关掉新架构,不要硬上。
最后聊一点我自己的工作习惯:我会在项目根目录维护一个DEPS.md,记录每个依赖的用途、锁定的版本、为什么选它、升级时需要注意什么。每次有人问我“这个项目都用了啥”,我不用翻 package.json,直接把这个文件甩过去就行。团队协作里,这块信息比代码本身更值钱,因为它避免了下一个人把版本乱升级、把原生配置删掉,然后集体加班排查的悲剧。你在 SDK54 这种新基线上搞依赖,也同样建议留下这份“为什么”的记录。