React Native本地通知实战:JNI桥接C++调度算法与CLion环境配置

📅 发布时间:2026/9/20 3:08:41
React Native本地通知实战:JNI桥接C++调度算法与CLion环境配置
做React Native开发时间久了你会发现一个规律凡是跟系统底层能力沾边的需求社区里永远能找到“看起来能用”的库但真到上线那一刻问题全都暴露出来。本地通知就是典型。我最近在做一个日程提醒类的App本地通知是核心中的核心用户杀掉App之后该弹的提醒必须照弹不误。一开始我用纯JS定时器App一退到后台就失效换成社区通知库Android上勉强能跑但iOS的触发策略和自定义样式又不够灵活。最后只能自己写原生模块而且为了复用团队现有的C调度算法模块还把JNI也牵了进来顺手在CLion里搭了一套完整的JNI开发环境。这篇文章会完整记录这一路走过来的过程从本地通知的需求拆解、原生模块实现到JNI桥接调度算法再到CLion配置JNI环境的完整步骤最后是RN侧整合和启动白屏的排查。适合正在做RN加原生混合开发、或者准备在RN项目里接底层能力的同学参考尤其是Android侧要跟C/C打交道的那种场景。1. 为什么本地通知会牵扯到JNI很多人看到“React Native本地通知与JNI”这个组合会有点懵本地通知明明是系统API的事情跟JNI这种Java和C/C之间的桥梁有什么关系这个疑问很正常我在动手之前也花了不少时间想清楚这个问题。1.1 原生通知、本地通知与JNI三者的关系先明确一个概念本地通知Local Notification和远程推送Remote Push Notification不是一回事。远程推送需要服务器配合走APNs或者FCM要有网络本地通知则完全由客户端自己发起到点之后系统负责展示理论上App进程被杀掉也能触发。React Native里做本地通知通常有三条路线JS定时器 通知API用setTimeout或setInterval在JS层计算时间到点之后调用通知库展示。问题很明显——App进后台之后JS定时器会被系统挂起进程被杀就更不用说了这是个“看起来能用实际不可靠”的方案。社区通知库比如notifee、react-native-notifications。这些库在原生层封装了通知能力可靠性比纯JS方案好不少绝大多数App用它们就够了。但如果你需要非常规的触发规则、复杂的重复策略或者想在通知之前做大量本地计算库的抽象层反而会成为限制。自己写原生模块在Java/Kotlin和Swift/Objective-C里直接调用系统通知API完全掌控触发逻辑。这样的好处是可靠性和灵活度拉满坏处是要处理两套原生代码。JNI就是在走第三条路线时被牵扯进来的。我遇到的情况是这样的团队内部已经有一套用C写的调度引擎负责根据用户的作息习惯、节假日、用药周期等规则计算“下一次提醒应该在什么时间”。这套引擎在iOS端可以直接用Xcode天然支持混编C但Android端没法直接用Java重写一遍——几千行调度算法重写成Java不仅工作量大而且两边逻辑一旦出现偏差排查起来会非常痛苦。于是JNI就派上了用场C调度引擎通过JNI暴露给Java层调用Java层负责调用AlarmManager设置闹钟闹钟到点触发BroadcastReceiver由系统展示本地通知。1.2 从“能用”到“可靠”本地通知的四个硬指标做本地通知核心不是能不能弹出来而是能不能“可靠地、准确地、按规则地”弹出来。我总结了四个硬指标每个指标背后都是一堆坑指标说明常见方案的短板进程存活App被用户杀掉后仍能触发JS定时器失效部分库依赖后台任务时间精度到点时间误差尽量小系统省电策略会导致延时重复规则支持每天、每周、自定义复杂规则社区库的重复策略不够灵活渠道样式Android通知渠道、声音、振动、角标直接写原生代码才能完全控制如果你的App只是“明天早上8点提醒我喝水”那用社区库就够了。但如果你要做的是“每两小时提醒一次工作日跳过午休时间法定节假日顺延”这套规则在C引擎里早就写好了你要做的不是用Java再写一遍而是用JNI把C的运行结果接过来。这也引出一个关键判断JNI不是本地通知的必需品而是你在“复用C逻辑”这个需求下的必然选择。如果不需要复用底层算法没有历史包袱不建议主动引入JNI毕竟每多一层桥接就多一分调试成本。2. RN本地通知原生模块的完整实现方案决定走原生模块路线之后接下来的问题是原生层到底要接管哪些事情JS层还需要保留什么职责这个边界划清楚后面写代码才不会乱。2.1 通知能力拆解原生代码需要接管哪些事我把本地通知的能力拆成了五块权限申请Android 13及以上需要动态申请POST_NOTIFICATIONS权限Android 8及以上需要创建NotificationChanneliOS需要请求UNUserNotificationCenter授权。时间计算根据规则算出下一次触发时间这部分由C调度引擎通过JNI完成。系统调度Android用AlarmManager的RTC_WAKEUP类型设置精确闹钟iOS用UNTimeIntervalNotificationTrigger或UNCalendarNotificationTrigger。消息展示构造通知内容、渠道、点击行为交给系统展示。点击分发用户点击通知后把事件带回RN层JS端可以跳转到对应页面。这套划分的核心思路是JS层只负责“告诉原生层需要一条什么规则的通知”剩下的权限、调度、展示全部交给原生层。这样业务侧永远不需要关心系统差异。2.2 Android端原生模块代码流程Android端我用Java写了一个ReactContextBaseJavaModule子类通过ReactMethod暴露给JS。核心流程分三步创建通知渠道、计算触发时间、设置AlarmManager。创建通知渠道和权限请求放在模块初始化时做public class NotificationModule extends ReactContextBaseJavaModule { private static final String CHANNEL_ID scheduler_channel; private final ReactApplicationContext reactContext; public NotificationModule(ReactApplicationContext reactContext) { super(reactContext); this.reactContext reactContext; createNotificationChannel(); } Override public String getName() { return NotificationScheduler; } private void createNotificationChannel() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { NotificationChannel channel new NotificationChannel( CHANNEL_ID, 日程提醒, NotificationManager.IMPORTANCE_HIGH ); channel.setDescription(根据用户作息规则触发的提醒); NotificationManager manager reactContext.getSystemService(NotificationManager.class); manager.createNotificationChannel(channel); } } }这里有几个容易忽略的细节。Android 8.0以下的设备没有NotificationChannel概念所以创建渠道必须判断SDK版本。Android 13虽然会自动弹权限弹窗但前提是你必须在AndroidManifest里声明POST_NOTIFICATIONS权限并且通过ActivityCompat.requestPermissions在运行时申请否则通知会被系统静默丢弃。然后是核心的调度方法我暴露了一个scheduleNotification方法给JS调用ReactMethod public void scheduleNotification(String title, String body, double timestamp, Promise promise) { try { long triggerAt (long) timestamp; // 这里就是JNI的入口C调度引擎根据规则计算下次触发时间 long calculatedTime NotificationScheduler.calculateNextTriggerTime( triggerAt, 1, weekday_interval_2h ); Intent intent new Intent(reactContext, NotificationReceiver.class); intent.putExtra(title, title); intent.putExtra(body, body); int requestCode (int) (System.currentTimeMillis() % Integer.MAX_VALUE); PendingIntent pendingIntent PendingIntent.getBroadcast( reactContext, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); AlarmManager alarmManager (AlarmManager) reactContext.getSystemService(Context.ALARM_SERVICE); alarmManager.setExactAndAllowWhileIdle( AlarmManager.RTC_WAKEUP, calculatedTime, pendingIntent ); promise.resolve(requestCode); } catch (Exception e) { promise.reject(NOTIFICATION_SCHEDULE_ERROR, e); } }这里要重点说两个问题。setExactAndAllowWhileIdle是Android 6.0之后处理Doze模式的关键API。如果不用它设备息屏几分钟后AlarmManager会把闹钟批量延迟到维护窗口通知就会迟到。但即使用了这个API国产ROMMIUI、EMUI、ColorOS等仍然可能因为后台省电策略拦截AlarmManager需要在各厂商的白名单机制里申请或者引导用户把App加入“不优化电池”列表。这块是Android本地通知最大的现实问题不是代码能完全解决的。另一个是PendingIntent的标志位。Android 12API 31开始强制要求FLAG_IMMUTABLE或FLAG_MUTABLE不写直接抛异常。老项目升级targetSdkVersion的时候很容易踩这个坑。NotificationReceiver是真正展示通知的组件需要在AndroidManifest里注册public class NotificationReceiver extends BroadcastReceiver { Override public void onReceive(Context context, Intent intent) { String title intent.getStringExtra(title); String body intent.getStringExtra(body); Intent launchIntent context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); PendingIntent contentIntent PendingIntent.getActivity( context, 0, launchIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE ); Notification notification new NotificationCompat.Builder(context, scheduler_channel) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(title) .setContentText(body) .setPriority(NotificationCompat.PRIORITY_HIGH) .setAutoCancel(true) .setContentIntent(contentIntent) .build(); NotificationManager manager (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) PackageManager.PERMISSION_GRANTED || Build.VERSION.SDK_INT Build.VERSION_CODES.TIRAMISU) { manager.notify((int) System.currentTimeMillis(), notification); } } }onReceive里用getLaunchIntentForPackage回到App主页实现“点击通知打开App”的基础行为。如果想要点击后跳转到指定页面需要在这里构造参数RN侧通过Linking监听来做路由分发。最后在MainApplication里注册模块Override protected ListReactPackage getPackages() { return Arrays.asList( new MainReactPackage(), new ReactPackage() { Override public ListNativeModule createNativeModules(ReactApplicationContext reactContext) { return Arrays.asList(new NotificationModule(reactContext)); } Override public ListViewManager createViewManagers(ReactApplicationContext reactContext) { return Collections.emptyList(); } } ); }2.3 iOS端本地通知的快速落地方案iOS侧没有JNI的概念Swift和Objective-C可以直接调用C代码这部分是天然的优势。但本地通知的API和Android差异很大核心是UNUserNotificationCenter。请求权限并注册通知import UserNotifications UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in if granted { DispatchQueue.main.async { UIApplication.shared.registerForRemoteNotifications() } } }设置定时触发let content UNMutableNotificationContent() content.title 日程提醒 content.body 该喝水了 content.sound .default let trigger UNTimeIntervalNotificationTrigger(timeInterval: 3600, repeats: false) let request UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if let error error { print(添加通知失败: \(error)) } }如果要做每天固定时间点的提醒用UNCalendarNotificationTrigger更合适var dateComponents DateComponents() dateComponents.hour 9 dateComponents.minute 0 let trigger UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)iOS这边最大的坑是如果用户杀掉App通知仍然能正常触发但如果用户把App从后台划掉之前先长按App图标选择了“关闭”部分iOS版本会有通知延时的现象。另外UNTimeIntervalNotificationTrigger有个硬性限制repeats为true时timeInterval必须大于等于60秒否则会报错。3. JNI桥接把调度算法下沉到C/C层原生通知模块跑通之后JNI的引入就有了真实的载体。这一章我会把JNI的完整链路拆开来讲从Java层的native方法声明到头文件生成再到C实现和构建配置。3.1 为什么把调度算法放到C/C层而不是直接写在Java里这个问题我在做技术方案的时候纠结了很久最后选出三个核心理由跨平台复用是第一位。同一个调度引擎iOS直接编译CAndroid通过JNI调用。如果写成Java那iOS就得重写一遍Swift版本两边要维护同一套调度逻辑的两份实现任何一次规则变更都要同步改两端改完之后还要担心两边的边界条件是否一致。现在C写一份两边共用规则逻辑永远不会有偏差。纯算法代码在C里性能更好。我的调度引擎底层涉及日期计算、时区偏移、节假日判断、区间合并这些操作在C里用手术级的数据结构处理执行速度比Java版本快一个量级。虽然本地通知的调度计算不是高频操作但用户可能一次性创建几十条提醒规则批量计算时这个性能差距还是能感知到的。逻辑隔离让团队协作更顺畅。负责调度算法的同事不熟悉Android开发他只需要维护C代码和头文件跑通测试用例就行。我负责JNI适配层把算法结果转成Java能用的数据。这种清晰的职责划分比把算法逻辑混在业务代码里好维护得多。3.2 JNI接口定义与头文件生成先在Java层声明一个native方法作为C调度引擎的入口package com.example.notificationscheduler; public class NotificationScheduler { static { System.loadLibrary(scheduler); } public static native long calculateNextTriggerTime(long baseTime, int ruleType, String ruleValue); }编译成class之后用javac的-h参数生成JNI头文件。注意新版本的JDK已经移除了javah命令高版本JDK统一用javac -h替代javac -h . NotificationScheduler.java执行之后会生成一个com_example_notificationscheduler_NotificationScheduler.h文件内容长这样/* DO NOT EDIT THIS FILE - it is machine generated */ #include jni.h #ifndef _Included_com_example_notificationscheduler_NotificationScheduler #define _Included_com_example_notificationscheduler_NotificationScheduler #ifdef __cplusplus extern C { #endif JNIEXPORT jlong JNICALL Java_com_example_notificationscheduler_NotificationScheduler_calculateNextTriggerTime (JNIEnv *, jclass, jlong, jint, jstring); #ifdef __cplusplus } #endif #endif这里有个小知识jclass参数对应Java层static方法所属的类。因为calculateNextTriggerTime是static方法所以第二个参数是jclass如果是实例方法第二个参数就是jobject。3.3 C侧实现与内存管理新建CPP文件实现头文件里声明的函数。这里以“工作日每隔2小时提醒一次”的简化规则为例#include jni.h #include ctime #include com_example_notificationscheduler_NotificationScheduler.h extern C JNIEXPORT jlong JNICALL Java_com_example_notificationscheduler_NotificationScheduler_calculateNextTriggerTime( JNIEnv* env, jclass clazz, jlong baseTime, jint ruleType, jstring ruleValue) { const char* value env-GetStringUTFChars(ruleValue, nullptr); // 实际项目中这里是复杂的调度引擎调用 // 比如解析 ruleValue weekday_interval_2h // 根据当前时间和规则推算出下一次触发时间 time_t now static_casttime_t(baseTime); time_t nextTime now 2 * 60 * 60; // 简化为当前时间加2小时 env-ReleaseStringUTFChars(ruleValue, value); return static_castjlong(nextTime); }这里有几个JNI开发的硬性注意事项必须用extern C包裹否则C编译器会做Name ManglingJava虚拟机就找不到对应的导出符号了。JNIEnv指针不能跨线程缓存也不能被多个线程共享。每个线程调用native方法时拿到的JNIEnv都是线程独立的如果你在C里开启了一个新线程需要调用JavaVM的AttachCurrentThread来获取线程自己的JNIEnv。GetStringUTFChars拿到的字符串必须用ReleaseStringUTFChars释放。JNI的局部引用表是有容量上限的通常16个不释放的话局部引用表满了会直接崩溃。返回long类型时注意32位和64位平台差异jlong是64位有符号整数Android上基本都是64位不用太担心但在某些嵌入式平台上要小心。3.4 CMake构建配置JNI代码编译成动态库Android Studio通常用CMake。在app/src/main/cpp目录下放一个CMakeLists.txtcmake_minimum_required(VERSION 3.22.1) project(scheduler) add_library(scheduler SHARED scheduler.cpp include/scheduler_engine.cpp ) find_library(log-lib log) target_link_libraries(scheduler ${log-lib}) target_include_directories(scheduler PRIVATE src/main/cpp/include)然后在app/build.gradle里关联CMakeandroid { defaultConfig { externalNativeBuild { cmake { cppFlags -stdc17 arguments -DANDROID_STLc_shared } } } externalNativeBuild { cmake { path src/main/cpp/CMakeLists.txt } } }构建产物libscheduler.so会自动打包进APKJava层System.loadLibrary(scheduler)就能加载。4. 在CLion中配置JNI开发环境的完整记录这里要单独提一下CLion因为我发现很多人做RN开发时写原生模块是Android Studio一把梭但一旦涉及JNI的C调试Android Studio的代码分析和调试体验属实一般。我个人的做法是C代码在CLion里开发和调试通过CMake或预编译产物集成到Android工程。4.1 环境组件与版本匹配JNI开发的环境配置是版本敏感工程版本不匹配编译期可能没问题运行时各种莫名其妙的问题。我当前使用的稳定组合组件版本备注JDK17编译和javac -h生成头文件Android SDKAPI 34提供android.jarAndroid NDK25.2.9519653提供交叉编译工具链CMake3.22.1NDK自带也可用CLion内置Ninja1.10.2默认构建系统CLion2024.1需要能识别NDK工具链一个特别重要的建议JDK版本和NDK版本保持较新的稳定版即可但不要追最新。我试过NDK 23以下版本编译出来的so在Android 14设备上偶发崩溃NDK 26的RC版本又和CMake 3.22有兼容问题最终锁定25.2.x。这个版本组合实测下来是当前最稳的搭配。4.2 CLion中的Toolchain与CMake配置CLion默认只认得本机编译工具链要编译Android的so必须配置NDK工具链。打开CLion的Settings - Build, Execution, Deployment - Toolchains点击加号新建一个工具链Toolchain类型选择Android NDKNDK路径填NDK安装目录比如macOS下是/Users/你的用户名/Library/Android/sdk/ndk/25.2.9519653Windows下是C:\Users\你的用户名\AppData\Local\Android\Sdk\ndk\25.2.9519653CMake路径可选CLion通常自带CMake如果项目中用了特定的CMake版本可以手动指定SDK目录下的cmakeDebugger建议用NDK提供的lldb路径在NDK目录下的toolchains/llvm/prebuilt/ /bin/lldb工具链配好之后还需要配置CMake的Profile。Settings - Build, Execution, Deployment - CMake修改或新建一个ProfileToolchain选择刚才创建的NDK工具链CMake options填入-DCMAKE_TOOLCHAIN_FILE/Users/你的用户名/Library/Android/sdk/ndk/25.2.9519653/build/cmake/android.toolchain.cmake -DANDROID_ABIarm64-v8a -DANDROID_PLATFORMandroid-24 -DANDROID_STLc_sharedCMake options里这两个关键参数的作用ANDROID_ABI指定目标ABI。只填arm64-v8a的话编译出来的so只适配64位ARM设备。如果App要兼容32位设备需要分别编译armeabi-v7a和arm64-v8a两个版本或者让CMake遍历全部ABI。实话说现在新设备基本都是arm64如果你的App minSdkVersion在21以上且不追求老设备兼容只出arm64-v8a是完全可以接受的APK体积还能小一截。ANDROID_PLATFORM指定最低支持的Android API级别不是targetSdkVersion而是你在build.gradle里声明的minSdkVersion。填太高会导致低版本设备安装不上填太低又会限制你用新API通常填App的minSdkVersion对应的API号即可。配置完成后CLion就能识别NDK交叉编译环境编译、代码分析、调试功能都能正常使用了。有一点值得注意CLion的NDK调试需要配合真机或模拟器一起用。先在CLion里点击Build生成so然后把命令行构建的产物拷贝到Android工程的jniLibs目录。CLion本身不会直接把so推到设备里这是CLion和Android Studio在构建流程上的最大差异。如果不想手动拷贝可以在CMakeLists里加一个POST_BUILD的copy命令让每次构建完自动复制到指定目录。4.3 外部CMake构建so与Android工程集成CLion里走Build - Build Project构建产物默认输出到cmake-build-debug/目录下根据ABI生成对应的子目录结构。找到libscheduler.so之后把它复制到Android工程app/src/main/jniLibs/arm64-v8a/libscheduler.so app/src/main/jniLibs/armeabi-v7a/libscheduler.so如果项目用了externalNativeBuildAndroid Studio会在构建时自己调用CMake编译不需要手动复制。但我的经验是在CLion里调试好算法逻辑和JNI适配层之后把最终的CMake配置复制到Android工程让Gradle统一构建这样两边维护同一套CMakeLists.txt。CLion作为开发和调试工具Android Studio/Gradle作为最终构建工具。4.4 配置过程中最容易踩的几个坑这一套环境我前前后后折腾了两三天遇到过的问题值得列出来clang头文件找不到jni.h。CLion里代码分析报错编译却正常这是CLion的include路径没有同步NDK的sysroot导致的。解决办法是在工具链配置正确之后让CLion重新加载CMake项目File - Reload CMake Project让IntelliSense重新解析include路径。编译出来的so加载报UnsatisfiedLinkError。最常见的原因是ABI不匹配。Java层System.loadLibrary的时候系统会在APK的lib/ /目录下找so如果你的APK只打包了arm64-v8a用户在32位设备上装就会找不到。另一个原因是C层的导出符号名被编译器改了检查是否加了extern C。构建时提示NDK版本和CMake不兼容。这通常是装了多个NDK版本导致的CLion的工具链指向了一个版本CMake的toolchain文件又从另一个版本里读取参数。把Settings里能看到的NDK路径全部统一成同一个版本即可。CLion的Build按钮是灰色不可点的。这说明CMake Profile没有正确关联工具链回到CMake设置页检查Toolchain是否选到了NDK工具链。打开CLion加载整个Android工程时扫描巨慢。这不是必须的CLion对Android工程的Gradle支持远不如Android Studio我的做法是单独打开src/main/cpp目录作为项目根目录在CMakeLists.txt里手动include需要的源文件和头文件这样CLion的响应速度快得多。5. RN侧整合、真机验证与启动白屏排查原生模块和JNI桥接都搞定之后最后一步是把它们接到RN侧然后做真机验证。这个过程中还会顺带遇到启动白屏的问题这里一并说清楚。5.1 把原生模块暴露给JS侧在RN侧调用原生模块最简单的方式是NativeModulesimport { NativeModules } from react-native; const { NotificationScheduler } NativeModules; function scheduleReminder(title, body, timestamp) { NotificationScheduler.scheduleNotification(title, body, timestamp) .then(requestCode { console.log(提醒已设置requestCode:, requestCode); }) .catch(error { console.warn(设置提醒失败:, error); }); }这里有一个使用习惯上的建议。NativeModules直接访问原生模块时没有任何类型检查参数类型传错只能在运行时发现。我在项目里习惯再做一层JS封装把所有原生模块调用收口到一个文件里统一做参数校验和错误处理RN业务代码永远不直接接触NativeModules。如果项目用的是React Native 0.7x以上版本还可以考虑TurboModule。TurboModule的架构更先进支持同步调用性能更好但需要额外的配置成本。对于本地通知这种低频操作用NativeModules完全够用没必要为了技术先进性给自己找活干。5.2 真机验证的三条检查路径功能开发完后真机验证比写代码更能暴露问题。我把验证过程分成了三条路径每条都是实际项目中踩过的路径一进程生命周期测试这是本地通知最核心的验证App在前台、在后台、被用户从后台任务列表划掉之后通知是否都能正常触发。要覆盖以下场景测试场景预期结果实际情况App在前台到点弹通知可行App退到后台不杀进程到点弹通知可行App被杀掉到点弹通知Android依赖AlarmManageriOS依赖系统通知手机息屏放置一段时间通知尽量准时Doze模式可能延迟国产ROM可能拦截路径二时间与规则边界测试因为调度算法已经下沉到了C层这里要特别验证JNI传参和返回值是否正确。比如跨越午夜、跨时区、夏令时切换这些边界情况C引擎处理的结果通过JNI返回后Java侧是否正确传递给了AlarmManager。路径三重复通知与取消通知测试连续创建多条提醒规则验证是否互不干扰。取消时要通过requestCode精确取消不能把其他规则的通知一起取消了。5.3 顺带解决RN启动白屏做真机验证的时候我顺便被RN启动白屏折磨了一轮。现象是点击App图标之后有1到3秒的白屏然后才看到首页内容。这个问题在release包上比debug包更明显debug包背后有Metro的热更新服务用户感知到的“启动完成”时间被掩盖了。白屏的根因通常有两个一是JS Bundle体积大加载和解析耗时长二是原生层没有任何启动画面从window创建到ReactRootView渲染出第一帧之间程序显示的是默认的白色背景。Android端最简单的处理方式是通过主题给window设置一个启动背景让系统在RN渲染完成前显示这个背景图而不是白屏!-- res/values/styles.xml -- style nameAppTheme parentTheme.AppCompat.Light.NoActionBar item nameandroid:windowBackgrounddrawable/splash_background/item /stylesplash_background做成layer-list里面可以放Logo图和品牌色!-- res/drawable/splash_background.xml -- layer-list xmlns:androidhttp://schemas.android.com/apk/res/android item android:drawablecolor/brand_color / item bitmap android:gravitycenter android:srcdrawable/splash_logo / /item /layer-list这个方案的最大优势是不阻塞启动流程不会增加额外白屏时间因为windowBackground在window创建的第一时间就被绘制了。iOS侧对应的是LaunchScreen.storyboard在Xcode里配置好启动图片和背景色就行。需要注意的是iOS对Launch Screen有一套自己的规则模拟器上的表现可能和真机差异很大务必以真机为准。如果启动白屏是由JS Bundle加载太慢导致的单纯配背景图治标不治本。这时候要做的不是优化启动画面而是拆包、压缩Bundle体积、把启动时不需要的模块做懒加载。React Native新架构的TurboModule和Fabric在启动速度方面比旧架构好了不少有条件的话建议往新架构靠。我在实际开发中体会到RN启动白屏和本地通知是两件看似不相关、但同样影响用户感知的事情。一个应用如果冷启动总是白屏用户会认为App卡死如果该弹的通知不弹用户会认为App没有按时执行任务。这两件事都值得从原生层入手做扎实。最后还是那句话如果你只是在RN里处理简单的本地通知不一定非得上原生模块加JNI这一套组合拳notifee这类成熟的社区库完全能覆盖大部分场景。但如果你有跨平台复用的C逻辑、复杂的调度规则、或者对通知可靠性有极高的要求那这篇文章里记录的原生模块方案、JNI桥接方式、CLion环境配置和启动白屏排查思路应该能帮你少走不少弯路。踩过几次坑之后我最大的体会是技术选型没有最好的只有最合适的关键是搞清楚自己的需求到底在哪个层次。