React-Native 全方位异常监控
最近在做 RN 应用线上错误监控的需求,在此记录下常用方案。
#
0. 开始React Native 在架构上整体可分为三块:Native、JavaScript 和 Bridge。其中,Native 管理 UI 更新及交互,JavaScript 调用 Native 能力实现业务功能,Bridge 负责在二者之间传递消息。
最上层提供类 React 支持,运行在 JavaScriptCore
提供的 JavaScript 运行时环境中,Bridge 层将 JavaScript 与 Native 世界连接起来。
本文从以下三个角度,分别介绍如何捕获 RN 应用中未被处理的异常:
- Native 异常捕获;
- JS 异常捕获;
- React 异常捕获;
#
1. Native 异常捕获Native 有较多成熟的方案,如友盟、Bugly、网易云捕和 crashlytics 等,这些平台不仅提供异常捕获能力,还相应的有上报、统计、预警等能力。本文不对以上平台异常捕获实现方式进行分析,而是通过分析 react-native-exception-handler 了解 Native 端异常捕获的实现原理。 react-native-exception-handler 实现了 setNativeExceptionHandle
用于设置 Native 监测到异常时的回调函数,如下所示:
#
1.1 Android 异常捕获Android 提供了一个异常捕获接口 Thread.UncaughtExceptionHandler
用于捕获未被处理的异常。react-native-exception-handler 亦是基于此实现对 Android 端异常捕获的,其主要代码及分析如下所示:
可以看到,主要做了四件事:
- 实例化 new Thread.UncaughtExceptionHandler(),并重写其 uncaughtException 方法;
- uncaughtException 方法中执行 JS 回调函数;
- 兼容自定义 Native Exception handler 的情况;
- 调用 Thread.setDefaultUncaughtExceptionHandler 重置异常处理器;
#
1.2 iOS 异常捕获iOS 通常利用 NSSetUncaughtExceptionHandler
设置全部的异常处理器,当异常情况发生时,会执行其设置的异常处理器。react-native-exception-handler 也是基于此实现对 iOS 端异常的捕获,如下所示:
上述代码主要做了三件事:
- 设置异常处理函数用于执行 JS 回调;
- 获取已存在的 native 异常处理器;
- 利用 NSSetUncaughtExceptionHandler 自定义异常处理器 HandleException;
接下来,看下具体的 handleException
又做了些什么呢?
#
1.3 小结通过对 react-native-exception-handler 源码的解读,可以知道,Android 和 iOS 分别利用 Thread.UncaughtExceptionHandler
和 NSSetUncaughtExceptionHandler
实现对应用程序的异常捕获。需要注意一点的是,当我们重置异常处理器时,需要考虑到其已存在的异常处理逻辑,避免将其直接覆盖,导致其他监测处理程序失效。
#
2. React 异常捕获为了解决部分 UI 的 JavaScript 错误导致整个应用白屏或者崩溃的问题,React 16 引入了新的概念 —— Error Boundaries(错误边界)。
错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
借用 static getDerivedStateFromError()
和 componentDidCatch()
两个生命周期实现错误边界,当抛出错误后,使用 static getDerivedStateFromError()
渲染备用 UI ,使用 componentDidCatch()
打印错误信息。
note
错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误。错误边界无法捕获以下场景中产生的错误:
- 事件处理
- 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
- 服务端渲染
- 它自身抛出来的错误(并非它的子组件)
#
3. JS 异常捕获上文中提到,Error Boundaries 能捕获子组件生命周期函数中的异常,包括构造函数(constructor)和 render 函数。而无法捕获以下异常:
- 事件处理
- 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
- 服务端渲染
- 它自身抛出来的错误(并非它的子组件)
对于这些错误边界无法捕获的异常,在 web 中可以通过 window.onerror() 加载一个全局的error
事件处理函数用于自动收集错误报告。
那么 React Native 中是如何处理的呢?
#
3.1 BatchedBridgeReact Native 是通过 JS Bridge 处理 JS 与 Native 的所有通信的,而 JS Bridge (BatchedBridge.js)是 MessageQueue.js 的实例。
BatchedBridge 创建一个 MessageQueue 实例,并将它定义到全局变量中,以便给 JSCExecutor.cpp 中获取到。
#
3.2 MessageQueueMessageQueue 是 JS Context 和 Native Context 之间的唯一连接,如图,网络请求/响应、布局计算、渲染请求、用户交互、动画序列指令、Native 模块的调用和 I/O 的操作等,都要经过 MessageQueue 进行处理。开发中,可以通过调用 MessageQueue.spy 查看 JS <-> Native 之间的具体通信过程:
MessageQueue.js 有三个作用:
- 注册所有的 JavaScriptModule
- 提供方法供 c++ 端调用
- 分发 js 端 NativeModule 所有异步方法的调用(同步方法会直接调用c++端代码)
查看 MessageQueue.js 构造函数:
从 MessageQueue 源码中,可以看到,其定义了多个变量以及四个函数:
- callFunctionReturnFlushedQueue
- callFunctionReturnResultAndFlushedQueue
- flushedQueue
- invokeCallbackAndReturnFlushedQueue
而以上四个函数的调用时机则是交给 c++ 端 NativeToJsBridge.cpp,具体的通信机制可参考文章
继续阅读上述四个函数的实现,可以看到都调用了 MessageQueue 的私有方法 __guard:
代码很简单,可以看到 guard 会 根据 _shouldPauseOnThrow 的返回值决定是否对 fn 进行 try catch 处理,当 __shouldPauseOnThrow 返回 false 时,且 fn 有异常时,则会执行 ErrorUtils.reportFatalError(error) 将错误上报。
注释写的也很清晰,MessageQueue 设置了一个用于处理所有 JS 侧异常行为的处理器,并且可以通过设置 DebuggerInternal.shouldPauseOnThrow 来决定是否对异常进行捕获。
#
3.3 ErrorUtils当调用 ErrorUtils.reportFatalError(error)
时,若存在 __globalHandler 则执行 _globalHandler,并将错误信息作为参数传入。同时,ErrorUtils 提供了函数 setGlobalHandler 用于重置 _globalHandler。
#
3.4 demo那么 JS 的异常错误会被 MessageQueue 处理吗?我们可以开启 MessageQueue 看下其日志。
当点击屏幕按钮时,可在控制台上看到如下信息:
可以看到,当 JS 抛出异常时,会被 ErrorUtils 捕获到,并执行通过 global.ErrorUtils.setGlobalHandler 设置的处理函数。
note
0.64 版本开始,react-native pollfills 相关(包含 ErrorUtils 实现)已由 react-native/Libraries/polyfills
抽离为 @react-native/polyfills
#
4. Promise 异常捕获除了上述提到的几种导致 APP crash 或者崩溃的异常处理之外,当我们使用 Promise 时,若抛出异常时未被 catch 捕获或在 catch 阶段再次抛出异常,此时会导致后续逻辑无法正常执行。
在 web 端,浏览器会自动追踪内存使用情况,通过垃圾回收机制处理这个 rejected Promise,并且提供unhandledrejection
事件进行监听。
那么,那么在 React Native 中是如何处理此类 Promise 异常的呢?
在 RN 中,当遇到未处理的 Promise 异常时,控制台输出黄色警告⚠️:
而设备则表现为弹出黄屏:

查看源码 react-native/Libraries/Promise.js
可知,RN 默认在开发环境下,通过promise/setimmediate/rejection-tracking
去追踪 rejected 状态的Promise,并提供了onUnhandled
回调函数处理未进行处理的 rejected Promise:
其执行时机可以在rejection-tracking.js
中源码中找到:
那么,我们是否可以仿照 RN 的处理,自定义 Promise 的异常处理逻辑呢?答案当然可以了,直接从源码中 copy 并将其中的 onUnhandled 替换为自己的异常处理逻辑即可。
#
总结本文从 React Native 应用异常监控出发,基于 react-native-exception-handler
分析了 Native 侧异常捕获的常用方案,然后介绍了 React 利用错误边界处理组件渲染异常的方式,接着通过分析 React Native 中 MessageQueue.js 的源码引出调用 global.ErrorUtils.setGlobalHandler
捕获并处理 JS 侧的全局未捕获异常,最后提供了捕获 Promise Rejection 的方法。
文章的最后,提下本人实现的 react-native-error-helper,与 react-native-exception-handler 相比,去除了 Native 异常处理捕获
,在 JS 异常捕获
的基础上,添加了用于捕获 React 异常
的 错误边界组件 ErrorBoundary 和高阶组件 withErrorBoundary(hook useErrorBoundary 计划中),期待您的 star⭐️。
#
推荐阅读- React Native's re-architecture in 2020
- React Native 启动速度优化——Native 篇(内含源码分析)
- React Native 启动速度优化——JS 篇【全网最全,值得收藏】