先说个背景我最近把手上一个 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 具体小版本决定原生模块接口、新架构开关compileSdk33 ~ 35决定你能引用哪些 Android SDK APIminSdk24 及以上影响可用设备范围也影响三方库要求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 installAndroid 侧会自动链接。但有两个原生配置别忘gesture-handler 要求在 MainActivity 的onCreate里调用GestureHandlerEnabled相关的初始化不同版本写法不一样以官方文档为准screens 通常不需要额外初始化但如果启动时白屏或返回栈异常第一个怀疑对象就是它没配对版本。我这个工程里把这三个的版本都锁在兼容矩阵里react-navigation 主版本 7.x 时screens 用 4.xsafe-area-context 用 5.xgesture-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.gradleiOS 要手动把需要的字体文件加入 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) emptyListViewManager*, *() }在 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 这种新基线上搞依赖也同样建议留下这份“为什么”的记录。