资讯动态

React Native Android原生模块异步编排:Bolts Task实践指南

发布时间:2026/10/6 8:56:21 来源:尧图企业网站定制
做 React Native 的 Android 原生模块开发最头疼的往往不是某个 SDK 本身怎么调而是好几个异步任务凑在一起时怎么编排。早几年我负责 RN 端的原生能力层经常要串起“初始化推送 SDK → 拉取本地缓存配置 → 再并行刷新三个数据源 → 统一回传给 JS”这类流程用回调一层套一层代码丑、容易出错排查日志还得靠猜。后来把 Facebook 开源的 Bolts 库引进来整个任务编排才清爽起来。这篇就聊聊我在 React Native 的 Android 端集成 Bolts 的完整经验从 Task 与 Promise 的对应关系、链式调用、并行聚合到取消与生命周期再附上几个我实际踩过的坑希望能给同样在做原生模块或混合开发的兄弟一些参考。如果你还没接触过 Bolts可以先记住一句话它就是 Android 上的 Promise 实现只不过比 JS Promise 更“老派”更像一个直接映射 Java 编程习惯的任务调度器。它兼容老到不能再老的 API 14RN 0.40 之后的版本也在大量运用它的思路。下面的内容默认你已经会写基础的 RN 原生模块知道ReactContextBaseJavaModule和ReactMethod是怎么回事。1. 为什么会想到在 RN 的 Android 原生层用 Bolts1.1 一个真实的异步编排场景还是先讲个实际场景吧。当时我们 App 里的 RN 页面要展示订单列表进入页面前需要原生层帮忙完成三件事读取本地 token 并校验、初始化 IM 长连接 SDK、申请定位权限并获取一次精确位置。这三件事里有的是顺序依赖token 校验完才能启动 IM有的是可以并行的定位和其他两个互不干扰。假如全部用 Callback 写步骤 B 依赖步骤 A又要在 UI 线程做就得包一层runOnUiThread并行那两路要在回调里再包一层计数器任何一步报错都得在一个统一地方收集不然 JS 层根本不知道发生了啥这种代码不是不能写是写着写着就容易出现“回调里套回调里套回调”团队里别人接手时维护成本特别高。我用 Bolts 之后问题变成了这种形态Task.callInBackground(() - loadToken()) .onSuccessTask(token - initIM(token)) .onSuccessTask(ignored - Task.whenAll( Arrays.asList(refreshLocation(), fetchUserInfo()) )) .continueWith(task - { promise.resolve(resultOf(task)); return null; });从结构上说顺序、并发、错误处理在一条链里摆得明明白白。你只需要看一眼这段代码就能知道业务上有哪些依赖关系、哪些能并行跑、最终结果往哪走。对比一下改造成本和后续收益是值得的。1.2 Bolts 的核心概念速记Bolts 的老家是 Facebook 的 Parse 团队后来随 Parse 开源的一部分一直维护至今。Android 端通常叫 Bolts-Android又被拆成bolts-tasks和bolts-applinks。我们 RN 开发基本只用bolts-tasks也就是 Task 相关的部分。核心概念其实只有四个Task代表一个异步操作的结果可以理解为 Android 版的 Promise。它不像 Promise 那样天然是热任务创建 Task 本身不代表任务已经启动需要配合TaskCompletionSource或callInBackground启动。TaskCompletionSource手动控制的“启动器”持有 Task 引用由你决定什么时候setResult或setError。ContinuationTask 完成后的回调函数通过continueWith、onSuccess、continueWhile等方式挂载。CancellationTokenSource / CancellationToken取消信号用于在任务运行途中主动中断。跟 JS Promise 最大的不同是Bolts 的每个 continuation 可以指定执行线程。默认是在任务完成时的那个线程继续执行但你完全可以通过Task.UI_THREAD_EXECUTOR把回调扔回主线程。这一点在 Android 这种“UI 操作必须主线程耗时操作必须后台线程”的环境里真的很方便。1.3 为什么不直接用 Java 8 的 CompletableFuture 或 RxJava如果你现在问我这个问题很正常因为 CompletableFuture 确实也做得不错。但放在 RN 原生模块这个特定场景Bolts 有几个不可替代的考虑维度Bolts TaskCompletableFutureAPI Level没有最低 API 限制老项目友好需要 API 24 或用 desugaring老项目麻烦线程调度continueWith直接指定 Executor顺手要手动拎出 executor略绕与 RN Promise 交互结构对称几乎一一对应也能映射但多了一层类型转换依赖体积bolts-tasks只有几十 KB几乎没有成本JDK 自带无体积成本再说 RxJava那是个大而全的响应式框架如果项目里顺便要用 Observable 做流式数据当然可以但仅为了做任务编排、错误处理和并行聚合引入一套完整的 RxJava 链多少有点杀鸡用牛刀。Bolts 的定位恰恰是“小而美的 Promise 调度器”重心全在任务本身。我在 RN 模块里需要的不是响应式流而是把几个异步步骤稳定地串起来所以选它。1.4 它与 RN 的 Promise 到底如何对接稍等这里要先把关系理清。RN 的 JS 层有 Promise原生 Java 层如果希望 JS 能await nativeMethod()方法签名里需要声明com.facebook.react.bridge.Promise参数。你可以在原生方法里直接调promise.resolve也可以在内部把 Bolts 的 Task 执行完后再 resolve。所以 Bolts 和 RN Promise 不是替代关系而是“后台任务调度 前台结果通信”的配合关系。理解这一点后面所有代码就好懂了。2. 先从工程接入开始依赖与最小封装2.1 Gradle 依赖该加哪一行注意别一股脑把整个 Bolts 都拉进来。比较省心的做法是在android/app/build.gradle的 dependencies 里加implementation com.parse.bolts:bolts-tasks:1.4.0如果你只加bolts-android它会把bolts-applinks也带进来里面包含 manifest 相关的自动处理组件对我们纯做 RN 模块的人来说基本用不上偶尔还会和项目里已有的 ContentProvider 声明打架。所以优先只引bolts-tasks。同步完成后在 Android Studio 里一眼就能确认库进来了外部依赖里会出现com.parse.bolts:bolts-tasks:1.4.0。2.2 RN 原生模块的样板结构我们先准备一个最普通的原生模块叫BoltsDemoModulepublic class BoltsDemoModule extends ReactContextBaseJavaModule { private final ReactApplicationContext reactContext; public BoltsDemoModule(ReactApplicationContext reactContext) { super(reactContext); this.reactContext reactContext; } Override public String getName() { return BoltsDemo; } ReactMethod public void fetchToken(Promise promise) { // 后续把 Bolts 写进来 } }不要忘记在 Package 里注册这个模块否则 JS 侧NativeModules拿不到对象。注册时通常是在createNativeModules里return Arrays.asList(new BoltsDemoModule(reactContext))。这个问题不大但初学者最容易漏白屏排查又查不到原生报错时先回头看这一步。JS 侧这样调用import { NativeModules } from react-native; const result await NativeModules.BoltsDemo.fetchToken();原生方法一旦有Promise参数RN 的 bridge 就自动认为这是异步方法。你可以在方法体里直接 resolve 或 reject也可以把它交给任意后台线程去完成。关键是别阻塞 JS 线程别把 100ms 以上的操作放在方法体主流程里直接跑。2.3 第一个封装读取 SharedPreferences 再回传最容易理解的例子是读本地缓存。假设我们要在后台线程读 token读完通过 Promise 返回给 JS。用 Bolts 写ReactMethod public void fetchToken(Promise promise) { Task.callInBackground(() - { SharedPreferences sp reactContext.getSharedPreferences(rn_config, Context.MODE_PRIVATE); return sp.getString(access_token, ); }).continueWith(task - { if (task.isFaulted()) { promise.reject(FETCH_TOKEN_ERROR, task.getError()); } else { promise.resolve(task.getResult()); } return null; }); }这里的Task.callInBackground会丢到 Bolts 自己的全局后台线程池执行IO 操作不会卡住 JS 线程。continueWith的返回值用 null 就行因为我们已经把结果交给 Promise不再需要新的 Task 向下传播。如果你写 Java 8 lambda 不顺手也可以写匿名内部类效果一样。提示不要在主线程直接读 SharedPreferences。早期有的项目这么干小文件没问题但团队里一旦有人往里面写大对象UI 掉帧和 ANR 就来了。用 Bolts 把这类 IO 丢到后台线程是成本最低的解法。3. 链式调用与错误传播把回调地狱捋直3.1 串行场景token 校验完再初始化 IM回到开头的业务。第一步校验 token第二步初始化 IM必须顺序执行第二步的参数依赖第一部的返回结果。Bolts 里最合适的是onSuccessTaskTask.callInBackground(this::validateToken) .onSuccessTask(token - initIMConnection(token)) .continueWith(task - { if (task.isFaulted()) { promise.reject(INIT_FAILED, task.getError()); } else { promise.resolve(true); } return null; });onSuccessTask的含义是上一个 Task 成功完成后进入这个方法你返回一个新的 Task作为流水线上的下一道工序。如果上一步失败了整个链会直接跳到continueWith的错误处理中间步骤全部跳过。这个行为跟 JS Promise 的then是一致的直觉上很容易迁移。3.2 三种 continuation 的取舍刚开始用 Bolts最容易混的是continueWith、onSuccess、continueWhile到底该用哪个。我说一下日常经验方法适用场景注意continueWith无论成功失败都要执行适合统一收尾、释放资源、上报日志必须自己判断isFaulted/isCancelledonSuccessTask上一步成功后继续执行下一步并返回新 Task最常用写顺序逻辑推荐onSuccess上一步成功后返回一个普通值不需要新 Task适合转换结果、做纯计算continueWhile根据条件循环执行某异步任务类似 async/await 里的 while注意要有出口别写成死循环这里有个细节continueWith里面如果直接返回一个非 null 的 Task那个 Task 会成为下一个 continuation 的上一步。用 null 表示“不关心后续链”。如果是在链的末尾可以返回 null。如果在中间返回 null却还想继续往下链Bolts 会自动把 null 包成一个已完成 Task。所以不要怕链不会断。3.3 错误传播不用到处 catchBolts 的设计里异常沿着 Task 链一路向下传播直到某个 continuation 显式处理。我们在 RN 原生层只需要在链的末端统一转成 Promise 的 reject 就好。那些真正需要特殊处理的错误才在中间加一个onError或continueWith里按isFaulted()分支处理。Task.callInBackground(this::validateToken) .onSuccessTask(token - initIMConnection(token)) .continueWith(task - { if (task.isCancelled()) { promise.reject(TASK_CANCELLED, task has been cancelled); } else if (task.isFaulted()) { Throwable error task.getError(); promise.reject(TASK_FAILED, error.getMessage(), error); } else { promise.resolve(task.getResult()); } return null; });注意 reject 的第三个参数可以带 ThrowableJS 侧的error对象里会自动带上nativeStackAndroid排查原生异常时会方便不少。这是我后来才学到的用法。如果只传 message错误堆栈信息会少一大截。3.4 说一个线程切不过来的典型事故链式调用写顺手以后有个坑特别容易犯continueWith的默认执行线程是上一个任务结束的线程也就是后台线程池的线程。如果你在 continuation 里直接操作reactContext.getCurrentActivity()并更新 UI大概率在 Android 高版本上会收到CalledFromWrongThreadException。修正方式有两种。第一种用Task.UI_THREAD_EXECUTORcontinueWith(task - { // 更新道具或 UI 状态 return null; }, Task.UI_THREAD_EXECUTOR)第二种是把 UI 操作丢给 RN 的UiThreadUtil.runOnUiThread。二者选哪个都行我习惯在原生模块里统一用Task.UI_THREAD_EXECUTOR因为它的语义和前面整段链式调度保持一致代码里不会一会儿 Runnable 一会儿 Task读起来更舒服。4. 并行聚合、竞速与取消RN 场景里的高级玩法4.1 用 whenAll 把一路并行收拢回来和 JS 的Promise.all对应Bolts 有Task.whenAll。但有个细节要提醒Task.whenAll(List? extends Task?)返回的是TaskVoid它只能告诉你“全都完成了”不会替你把每个 Task 的结果重新收集成一个 List。想拿到每个任务的结果常见做法是让每个 Task 自己把结果写进预先准备好的下标数组或并发安全的容器里。final int N 3; final Object[] results new Object[N]; ListTaskVoid tasks Arrays.asList( Task.callInBackground(() - { results[0] loadUserInfo(); return null; }), Task.callInBackground(() - { results[1] loadSettings(); return null; }), Task.callInBackground(() - { results[2] loadDeviceFingerprint(); return null; }) ); Task.whenAll(tasks) .continueWith(task - { if (task.isFaulted()) { promise.reject(LOAD_DATA_FAILED, task.getError()); } else { promise.resolve(results); } return null; });每个分支独立抛错只要有一个任务失败whenAll返回的 Task 也会进入 faulted 状态。如果业务上想“部分成功也接受”可以对每个子任务悄悄捕获错误把错误标记也放进结果数组里由 JS 层去判断。这个取舍要看场景支付类必须全成功后放行列表展示类可以考虑“能拿到多少展示多少”。4.2 whenAny主备接口竞速的降级策略有些场景下我们需要先到先得例如主接口慢、备用接口快希望 JS 层尽快拿到一个可用结果。Task.whenAny就是干这个的它返回TaskTask?后者表示最先完成那个任务。TaskTask? any Task.whenAny( Arrays.asList(mainRequest(), backupRequest()) ); any.continueWith(task - { if (task.isFaulted()) { promise.reject(ALL_REQUESTS_FAILED, task.getError()); return null; } Task? winner task.getResult(); if (winner.isFaulted()) { promise.reject(WINNER_FAILED, winner.getError()); } else { promise.resolve(winner.getResult()); } return null; });这里要稍微留意getResult()的类型。因为两个请求类型可能不同whenAny给的TaskTask?是通配外形拿到的winner是Task?做强转之前最好用instanceof或断言确认。我在生产代码里通常先查winner.isFaulted()再强转到具体类型避免 ClassCastException。4.3 取消信号组件卸载时别再让人家干活了RN 页面里组件销毁后原生任务其实还在后台跑这是一种隐性资源浪费尤其当任务里带着网络连接或定位请求时效果特别明显。Bolts 的CancellationTokenSource可以做到“主动喊停”。原生模块里维护一个CancellationTokenSourceprivate CancellationTokenSource cts new CancellationTokenSource(); ReactMethod public void startHeavyTask(Promise promise) { Task.callInBackground(() - { while (true) { if (cts.getToken().isCancellationRequested()) { return null; } // 做点耗时的循环 } }).continueWith(t - { if (t.isCancelled()) { promise.reject(TASK_CANCELLED, cancelled by user); } else if (t.isFaulted()) { promise.reject(TASK_FAILED, t.getError()); } else { promise.resolve(true); } return null; }); } ReactMethod public void cancelHeavyTask() { cts.cancel(); }JS 侧在组件componentWillUnmount或useEffect的清理函数里调用NativeModules.BoltsDemo.cancelHeavyTask()即可。注意取消不是中断正在执行的线程它更像一个协作式标记任务体内部需要周期性检查isCancellationRequested()才能优雅退出。如果任务是阻塞调用比如死等某个回调取消机制帮不了太多那就要靠超时策略来补位。4.4 超时怎么处理Bolts 没有内置的timeoutAPI这也是它保持“小而美”的原因之一。我们可以用一个小技巧实现超时把原始任务和Task.delay(3000)塞进whenAny谁先完成谁说了算。TaskTask? race Task.whenAny( Arrays.asList(originalTask, Task.delay(3000)) ); race.continueWith(task - { Task? winner task.getResult(); if (winner originalTask) { promise.resolve(originalTask.getResult()); } else { promise.reject(TIMEOUT, task timeout after 3s); } return null; });注意originalTask要引用同一个 Task 实例不要新建一个Task.callInBackground(...)否则对象相等判断对不上。这种写法简单但不算完美原始任务超时后没有被取消它仍在后台跑。真要严格取消还是需要配合CancellationTokenSource让任务体内部响应取消信号。5. 踩坑记录从白屏、线程错乱到 Promise 重复回调5.1 启动白屏和初始化任务调度搜索“react native 启动白屏”的人应该不在少数。这个问题的本质是JS 引擎从加载 Bundle 到首帧渲染是需要时间的原生初始化和网络请求如果也在同一时间段内抢舞台白屏会更明显。我们当时的处理方式是在ReactActivity创建时用原生侧把最关键的三个初始化动作读取本地配置、建立长连接、初始化埋点 SDK通过 Bolts 串成一个 Task 链先跑掉等 JS 侧首帧准备好需要数据时这些初始化已经完成了一半以上。如果实在保底原生层可以先显示一个轻量的 ProgressBar 兜住用户视线首帧渲染完再移除。一个可复用的思路是把初始化任务放进一个静态的Task单例页面反复进出不用重复执行private static volatile TaskVoid sharedInitTask; private static TaskVoid ensureSharedInit() { if (sharedInitTask null) { synchronized (BoltsDemoModule.class) { if (sharedInitTask null) { sharedInitTask Task.callInBackground(() - { initPushSDK(); initIM(); initBury(); return null; }); } } } return sharedInitTask; }这种“初始化前置”的思路本质上是把耗时尽量前移给 JS 首帧争取时间窗口。Bolts 的价值在于我们可以把原本散落的初始化逻辑按依赖顺序、并发关系统一表达而不是在 Activity 生命周期里插一堆自增自减的状态标记。5.2 主线程 executor 与 RN bridge 的冲突有一次 QA 反馈某个原生方法调用后 JS 回调永远不执行。排查了半天发现我在continueWith(..., Task.UI_THREAD_EXECUTOR)里调用了promise.resolve。理论上 promise.resolve 可以在任意线程执行但当任务量激增、主线程被 UI 帧卡住时这种回调会排队等待导致 JS 侧看起来像“卡死了”实际是主线程迟迟处理不到这条 resolve。排查链路是这样的先看 Logcat确认原生方法已经走到promise.resolve之后再到 JS 侧加超时日志确认回调没进 JS最后怀疑是不是主线程被占满抓了一份 Trace 文件果然主线程上有大段 UI 测量和布局的耗时。应对方式也很简单凡是纯数据返回、不需要碰 UI 的 Promise 回调尽量在后台线程直接 resolve不要先用UI_THREAD_EXECUTOR绕一圈。只有确实要更新原生 UI 组件的内容才切主线程。这也再一次提醒搞清楚“回调里到底要干什么”比背执行规则更重要。5.3 Promise 重复 resolve / reject 引发的崩溃Task 链在并行场景下的一个隐患是当你用whenAny或两个分支同时收尾时可能不小心对同一个 Promise 调用了两次 resolve/reject。RN 的 Bridge 对重复 settle 的处理在不同版本上不一样——有的版本直接静默忽略有的版本会抛异常表现为 Java 层IllegalStateException: Promise already settled。我自己的做法是加一个AtomicBooleanprivate final AtomicBoolean settled new AtomicBoolean(false); private void safeResolve(Promise promise, Object result) { if (settled.compareAndSet(false, true)) { promise.resolve(result); } } private void safeReject(Promise promise, String code, Throwable t) { if (settled.compareAndSet(false, true)) { promise.reject(code, t.getMessage(), t); } }凡是可能多路返回的方法都先用它包一层。代码看起来多三行却省了不少线上崩溃采集。尤其当你做超时竞速时原始任务晚到一步但晚到的那一步里如果还有promise.resolve就很容易踩中重复 settle。5.4 内存泄漏continuation 的生命周期比 Activity 长最后一个坑尤其隐蔽。Bolts 的continueWith会持有 continuation 对象而 continuation 里如果直接引用了 Activity 或某个大型视图任务还没跑完Activity 已经销毁内存就一直被吊着。旋转屏幕几次后Dump Java Heap 能明显看到一堆旧 Activity 实例。修正习惯有两条continuation 里优先通过WeakReferenceT取外部对象或者只依赖ReactApplicationContext这类 App 级单例。在页面销毁时把对应的CancellationTokenSource.cancel()调出来让链快速进入 cancelled 分支释放 continuation。很多团队把 RN 原生模块写得“接口能用就行”不太关心生命周期但线上 OOM 往往就是这类细节一天天堆出来的。原生模块本身虽然是单例可它内部的异步任务链却完全可以超出模块所在页面的生命周期这一点必须时刻心里有数。最后再分享一个小技巧在 RN 和原生交互的这一层我后来把 Bolts 用成了一个统一的“原生异步编排层”凡是要做异步组合的原生方法对外都只暴露一个 Promise 接口内部一律用 Task 串并行。这样 JS 侧看到的永远是清晰的异步接口原生侧又不会退化成回调地狱。如果你也在做 RN 的 Android 原生模块可以先把这三个能力练熟callInBackground onSuccessTask组成串行链whenAll whenAny处理并行聚合CancellationTokenSource管生命周期之后再遇到的复杂流程基本都能在这套骨架上搭出来。希望这篇文章能帮你少踩几个我踩过的坑。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑