+-
?React Native 全方位异常监控
首页 专栏 react.js 文章详情
0
头图

🔥React Native 全方位异常监控

Chengbo 发布于 4 月 29 日
最近在做 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 监测到异常时的回调函数,如下所示:

export const setNativeExceptionHandler = (customErrorHandler = noop, forceApplicationToQuit = true, executeDefaultHandler = false) => { if (typeof customErrorHandler !== "function" || typeof forceApplicationToQuit !== "boolean") { console.log("setNativeExceptionHandler is called with wrong argument types.. first argument should be callback function and second argument is optional should be a boolean"); console.log("Not setting the native handler .. please fix setNativeExceptionHandler call"); return; } if (Platform.OS === "ios") { ReactNativeExceptionHandler.setHandlerforNativeException(executeDefaultHandler, customErrorHandler); } else { ReactNativeExceptionHandler.setHandlerforNativeException(executeDefaultHandler, forceApplicationToQuit, customErrorHandler); } };

1.1 Android 异常捕获

Android 提供了一个异常捕获接口 Thread.UncaughtExceptionHandler 用于捕获未被处理的异常。react-native-exception-handler 亦是基于此实现对 Android 端异常捕获的,其主要代码及分析如下所示:

@ReactMethod public void setHandlerforNativeException( final boolean executeOriginalUncaughtExceptionHandler, final boolean forceToQuit, Callback customHandler) { callbackHolder = customHandler; // 获取原有的异常处理器 originalHandler = Thread.getDefaultUncaughtExceptionHandler(); // 实例化异常处理器后,利用 setDefaultUncaughtExceptionHandler 重置异常处理器 Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { // 重写 uncaughtException 方法,当程序中有未捕获的异常时,会调用该方法 @Override public void uncaughtException(Thread thread, Throwable throwable) { String stackTraceString = Log.getStackTraceString(throwable); // 执行传入 JS 处理函数 callbackHolder.invoke(stackTraceString); // 用于兼容自定义 Native Exception handler 的情况,即通过 MainApplication.java 中 实例化 NativeExceptionHandlerIfc 并重写其 handleNativeException 方法。 if (nativeExceptionHandler != null) { nativeExceptionHandler.handleNativeException(thread, throwable, originalHandler); } else { // 获取 activity 并展示错误信息(一个弹窗,并提供重启和退出按钮) activity = getCurrentActivity(); Intent i = new Intent(); i.setClass(activity, errorIntentTargetClass); i.putExtra("stack_trace_string",stackTraceString); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); activity.startActivity(i); activity.finish(); // 允许执行且已存在异常处理函数时,执行原异常处理函数 if (executeOriginalUncaughtExceptionHandler && originalHandler != null) { originalHandler.uncaughtException(thread, throwable); } // 设置出现异常情况直接退出 if (forceToQuit) { System.exit(0); } } } }); }

可以看到,主要做了四件事:

实例化 new Thread.UncaughtExceptionHandler(),并重写其 uncaughtException 方法; uncaughtException 方法中执行 JS 回调函数; 兼容自定义 Native Exception handler 的情况; 调用 Thread.setDefaultUncaughtExceptionHandler 重置异常处理器;

1.2 iOS 异常捕获

iOS 通常利用 NSSetUncaughtExceptionHandler 设置全部的异常处理器,当异常情况发生时,会执行其设置的异常处理器。react-native-exception-handler 也是基于此实现对 iOS 端异常的捕获,如下所示:

// ==================================== // REACT NATIVE MODULE EXPOSED METHODS // ==================================== RCT_EXPORT_MODULE(); // METHOD TO INITIALIZE THE EXCEPTION HANDLER AND SET THE JS CALLBACK BLOCK RCT_EXPORT_METHOD(setHandlerforNativeException:(BOOL)callPreviouslyDefinedHandler withCallback: (RCTResponseSenderBlock)callback) { // 1.设置异常处理函数用于执行 JS 回调; jsErrorCallbackBlock = ^(NSException *exception, NSString *readeableException){ callback(@[readeableException]); }; // 2.获取已存在的 native 异常处理器; previousNativeErrorCallbackBlock = NSGetUncaughtExceptionHandler(); callPreviousNativeErrorCallbackBlock = callPreviouslyDefinedHandler; // 3. 利用 NSSetUncaughtExceptionHandler 自定义异常处理器 HandleException; NSSetUncaughtExceptionHandler(&HandleException); signal(SIGABRT, SignalHandler); signal(SIGILL, SignalHandler); signal(SIGSEGV, SignalHandler); signal(SIGFPE, SignalHandler); signal(SIGBUS, SignalHandler); //signal(SIGPIPE, SignalHandler); //Removing SIGPIPE as per https://github.com/master-atul/react-native-exception-handler/issues/32 NSLog(@"REGISTERED RN EXCEPTION HANDLER"); }

上述代码主要做了三件事:

设置异常处理函数用于执行 JS 回调; 获取已存在的 native 异常处理器; 利用 NSSetUncaughtExceptionHandler 自定义异常处理器 HandleException;

接下来,看下具体的 handleException 又做了些什么呢?

// ================================================================ // ACTUAL CUSTOM HANDLER called by the EXCEPTION AND SIGNAL HANDLER // WHICH KEEPS THE APP RUNNING ON EXCEPTION // ================================================================ - (void)handleException:(NSException *)exception { NSString * readeableError = [NSString stringWithFormat:NSLocalizedString(@"%@\n%@", nil), [exception reason], [[exception userInfo] objectForKey:RNUncaughtExceptionHandlerAddressesKey]]; dismissApp = false; // 1.允许执行且已存在异常处理函数时,执行原异常处理函数 if (callPreviousNativeErrorCallbackBlock && previousNativeErrorCallbackBlock) { previousNativeErrorCallbackBlock(exception); } // 2. 用于兼容自定义 Native Exception handler 的情况,可通过调用 replaceNativeExceptionHandlerBlock 实现 if(nativeErrorCallbackBlock != nil){ nativeErrorCallbackBlock(exception,readeableError); }else{ defaultNativeErrorCallbackBlock(exception,readeableError); } // 3. 执行 js 异常处理函数 jsErrorCallbackBlock(exception,readeableError); CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop); while (!dismissApp) { long count = CFArrayGetCount(allModes); long i = 0; while(i < count){ NSString *mode = CFArrayGetValueAtIndex(allModes, i); if(![mode isEqualToString:@"kCFRunLoopCommonModes"]){ CFRunLoopRunInMode((CFStringRef)mode, 0.001, false); } i++; } } CFRelease(allModes); NSSetUncaughtExceptionHandler(NULL); signal(SIGABRT, SIG_DFL); signal(SIGILL, SIG_DFL); signal(SIGSEGV, SIG_DFL); signal(SIGFPE, SIG_DFL); signal(SIGBUS, SIG_DFL); signal(SIGPIPE, SIG_DFL); kill(getpid(), [[[exception userInfo] objectForKey:RNUncaughtExceptionHandlerSignalKey] intValue]); }

1.3 小结

通过对 react-native-exception-handler 源码的解读,可以知道,Android 和 iOS 分别利用 Thread.UncaughtExceptionHandlerNSSetUncaughtExceptionHandler 实现对应用程序的异常捕获。需要注意一点的是,当我们重置异常处理器时,需要考虑到其已存在的异常处理逻辑,避免将其直接覆盖,导致其他监测处理程序失效。

2. React 异常捕获

为了解决部分 UI 的 JavaScript 错误导致整个应用白屏或者崩溃的问题,React 16 引入了新的概念 —— Error Boundaries(错误边界)。

错误边界是一种 React 组件,这种组件 可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

借用 static getDerivedStateFromError()componentDidCatch() 两个生命周期实现错误边界,当抛出错误后,使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。

class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // 更新 state 使下一次渲染能够显示降级后的 UI return { hasError: true }; } componentDidCatch(error, errorInfo) { // 你同样可以将错误日志上报给服务器 logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // 你可以自定义降级后的 UI 并渲染 return <Text>Something went wrong.</Text>; } return this.props.children; } }

错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误。错误边界无法捕获以下场景中产生的错误:

事件处理 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数) 服务端渲染 它自身抛出来的错误(并非它的子组件)

3. JS 异常捕获

上文中提到,Error Boundaries 能捕获子组件生命周期函数中的异常,包括构造函数(constructor)和 render 函数。而无法捕获以下异常:

事件处理 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数) 服务端渲染 它自身抛出来的错误(并非它的子组件)

对于这些错误边界无法捕获的异常,在 web 中可以通过 window.onerror() 加载一个全局的error事件处理函数用于自动收集错误报告。

那么 React Native 中是如何处理的呢?

3.1 BatchedBridge

React Native 是通过 JS Bridge 处理 JS 与 Native 的所有通信的,而 JS Bridge (BatchedBridge.js)是 MessageQueue.js 的实例。

'use strict'; const MessageQueue = require('MessageQueue'); const BatchedBridge = new MessageQueue(); Object.defineProperty(global, '__fbBatchedBridge', { configurable: true, value: BatchedBridge, }); module.exports = BatchedBridge;

BatchedBridge 创建一个 MessageQueue 实例,并将它定义到全局变量中,以便给 JSCExecutor.cpp 中获取到。

3.2 MessageQueue

MessageQueue 是 JS Context 和 Native Context 之间的唯一连接,如图,网络请求/响应、布局计算、渲染请求、用户交互、动画序列指令、Native 模块的调用和 I/O 的操作等,都要经过 MessageQueue 进行处理。开发中,可以通过调用 MessageQueue.spy 查看 JS <-> Native 之间的具体通信过程:

import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue'; MessageQueue.spy(true); // -or- // MessageQueue.spy((info) => console.log("I'm spying!", info));

MessageQueue.js 有三个作用:

注册所有的 JavaScriptModule 提供方法供 c++ 端调用 分发 js 端 NativeModule 所有异步方法的调用(同步方法会直接调用c++端代码)

查看 MessageQueue.js 构造函数:

constructor() { this._lazyCallableModules = {}; this._queue = [[], [], [], 0]; this._successCallbacks = []; this._failureCallbacks = []; this._callID = 0; this._lastFlush = 0; this._eventLoopStartTime = new Date().getTime(); if (__DEV__) { this._debugInfo = {}; this._remoteModuleTable = {}; this._remoteMethodTable = {}; } (this: any).callFunctionReturnFlushedQueue = this.callFunctionReturnFlushedQueue.bind( this, ); (this: any).callFunctionReturnResultAndFlushedQueue = this.callFunctionReturnResultAndFlushedQueue.bind( this, ); (this: any).flushedQueue = this.flushedQueue.bind(this); (this: any).invokeCallbackAndReturnFlushedQueue = this.invokeCallbackAndReturnFlushedQueue.bind( this, ); }

从 MessageQueue 源码中,可以看到,其定义了多个变量以及四个函数:

callFunctionReturnFlushedQueue callFunctionReturnResultAndFlushedQueue flushedQueue invokeCallbackAndReturnFlushedQueue

而以上四个函数的调用时机则是交给 c++ 端 NativeToJsBridge.cpp,具体的通信机制可参考文章

继续阅读上述四个函数的实现,可以看到都调用了 MessageQueue 的私有方法 __guard:

__guard(fn: () => void) { if (this.__shouldPauseOnThrow()) { fn(); } else { try { fn(); } catch (error) { ErrorUtils.reportFatalError(error); } } }

代码很简单,可以看到 __guard 会 根据 \___shouldPauseOnThrow 的返回值决定是否对 fn 进行 try catch 处理,当 __shouldPauseOnThrow 返回 false 时,且 fn 有异常时,则会执行 ErrorUtils.reportFatalError(error) 将错误上报。

// MessageQueue installs a global handler to catch all exceptions where JS users can register their own behavior // This handler makes all exceptions to be propagated from inside MessageQueue rather than by the VM at their origin // This makes stacktraces to be placed at MessageQueue rather than at where they were launched // The parameter DebuggerInternal.shouldPauseOnThrow is used to check before catching all exceptions and // can be configured by the VM or any Inspector __shouldPauseOnThrow(): boolean { return ( // $FlowFixMe typeof DebuggerInternal !== 'undefined' && DebuggerInternal.shouldPauseOnThrow === true // eslint-disable-line no-undef ); }

注释写的也很清晰,MessageQueue 设置了一个用于处理所有 JS 侧异常行为的处理器,并且可以通过设置 DebuggerInternal.shouldPauseOnThrow 来决定是否对异常进行捕获。

3.3 ErrorUtils

/** * This is the error handler that is called when we encounter an exception * when loading a module. This will report any errors encountered before * ExceptionsManager is configured. */ let _globalHandler: ErrorHandler = function onError( e: mixed, isFatal: boolean, ) { throw e; }; /** * The particular require runtime that we are using looks for a global * `ErrorUtils` object and if it exists, then it requires modules with the * error handler specified via ErrorUtils.setGlobalHandler by calling the * require function with applyWithGuard. Since the require module is loaded * before any of the modules, this ErrorUtils must be defined (and the handler * set) globally before requiring anything. */ const ErrorUtils = { setGlobalHandler(fun: ErrorHandler): void { _globalHandler = fun; }, getGlobalHandler(): ErrorHandler { return _globalHandler; }, reportError(error: mixed): void { _globalHandler && _globalHandler(error, false); }, reportFatalError(error: mixed): void { // NOTE: This has an untyped call site in Metro. _globalHandler && _globalHandler(error, true); }, ... }

当调用 ErrorUtils.reportFatalError(error) 时,若存在 __globalHandler 则执行 _globalHandler,并将错误信息作为参数传入。同时,ErrorUtils 提供了函数 setGlobalHandler 用于重置 _globalHandler。

global.ErrorUtils.setGlobalHandler(function (err) { consolo.log('global error: ', err); });

3.4 demo

那么 JS 的异常错误会被 MessageQueue 处理吗?我们可以开启 MessageQueue 看下其日志。

import React from 'react'; import { View, Text, } from 'react-native'; import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue'; MessageQueue.spy(true); // -or- MessageQueue.spy((info) => console.log("I'm spying!", info)); global.ErrorUtils.setGlobalHandler(function (err) { consolo.log('global error: ', err); }); const App = () => { const onPressButton = () => { throw new Error('i am error'); }; return ( <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}> <Text onPress={onPressButton}>按钮</Text> </View> ); };

当点击屏幕按钮时,可在控制台上看到如下信息:

可以看到,当 JS 抛出异常时,会被 ErrorUtils 捕获到,并执行通过 global.ErrorUtils.setGlobalHandler 设置的处理函数。

注意:0.64 版本开始,react-native pollfills 相关(包含 ErrorUtils 实现)已由 react-native/Libraries/polyfills 抽离为 @react-native/polyfills

4 Promise 异常捕获

除了上述提到的几种导致 APP crash 或者崩溃的异常处理之外,当我们使用 Promise 时,若抛出异常时未被 catch 捕获或在 catch 阶段再次抛出异常,此时会导致后续逻辑无法正常执行。

在 web 端,浏览器会自动追踪内存使用情况,通过垃圾回收机制处理这个 rejected Promise,并且提供unhandledrejection事件进行监听。

window.addEventListener('unhandledrejection', event => ···);

那么,那么在 React Native 中是如何处理此类 Promise 异常的呢?

在 RN 中,当遇到未处理的 Promise 异常时,控制台输出黄色警告⚠️:

而设备则表现为弹出黄屏:

<div align="center">

<img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b2415c6a36a5463f862879a3303fe416~tplv-k3u1fbpfcp-watermark.image" />

</div>

查看源码 react-native/Libraries/Promise.js 可知,RN 默认在开发环境下,通过promise/setimmediate/rejection-tracking去追踪 rejected 状态的Promise,并提供了onUnhandled回调函数处理未进行处理的 rejected Promise:

if (__DEV__) { require('promise/setimmediate/rejection-tracking').enable({ allRejections: true, onUnhandled: (id, error) => { const {message, stack} = error; const warning = `Possible Unhandled Promise Rejection (id: ${id}):\n` + (message == null ? '' : `${message}\n`) + (stack == null ? '' : stack); console.warn(warning); }, onHandled: (id) => { const warning = `Promise Rejection Handled (id: ${id})\n` + 'This means you can ignore any previous messages of the form ' + `"Possible Unhandled Promise Rejection (id: ${id}):"`; console.warn(warning); }, }); }

其执行时机可以在rejection-tracking.js中源码中找到:

//... timeout: setTimeout( onUnhandled.bind(null, promise._51), // For reference errors and type errors, this almost always // means the programmer made a mistake, so log them after just // 100ms // otherwise, wait 2 seconds to see if they get handled matchWhitelist(err, DEFAULT_WHITELIST) ? 100 : 2000 ), //...

那么,我们是否可以仿照 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 篇【全网最全,值得收藏】

参考文章

React Native 原理与实践 【从源码分析】可能是全网最实用的React Native异常解决方案 ReactNative 通信机制_c++端源码分析
react.js react-native
阅读 128 更新于 4 月 29 日
举报
收藏
分享
本作品系原创, 采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议
avatar
Chengbo

厚于德,诚于信,敏于行!

194 声望
11 粉丝
关注作者
0 条评论
得票数 最新
提交评论
avatar
Chengbo

厚于德,诚于信,敏于行!

194 声望
11 粉丝
关注作者
宣传栏
目录
最近在做 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 监测到异常时的回调函数,如下所示:

export const setNativeExceptionHandler = (customErrorHandler = noop, forceApplicationToQuit = true, executeDefaultHandler = false) => { if (typeof customErrorHandler !== "function" || typeof forceApplicationToQuit !== "boolean") { console.log("setNativeExceptionHandler is called with wrong argument types.. first argument should be callback function and second argument is optional should be a boolean"); console.log("Not setting the native handler .. please fix setNativeExceptionHandler call"); return; } if (Platform.OS === "ios") { ReactNativeExceptionHandler.setHandlerforNativeException(executeDefaultHandler, customErrorHandler); } else { ReactNativeExceptionHandler.setHandlerforNativeException(executeDefaultHandler, forceApplicationToQuit, customErrorHandler); } };

1.1 Android 异常捕获

Android 提供了一个异常捕获接口 Thread.UncaughtExceptionHandler 用于捕获未被处理的异常。react-native-exception-handler 亦是基于此实现对 Android 端异常捕获的,其主要代码及分析如下所示:

@ReactMethod public void setHandlerforNativeException( final boolean executeOriginalUncaughtExceptionHandler, final boolean forceToQuit, Callback customHandler) { callbackHolder = customHandler; // 获取原有的异常处理器 originalHandler = Thread.getDefaultUncaughtExceptionHandler(); // 实例化异常处理器后,利用 setDefaultUncaughtExceptionHandler 重置异常处理器 Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { // 重写 uncaughtException 方法,当程序中有未捕获的异常时,会调用该方法 @Override public void uncaughtException(Thread thread, Throwable throwable) { String stackTraceString = Log.getStackTraceString(throwable); // 执行传入 JS 处理函数 callbackHolder.invoke(stackTraceString); // 用于兼容自定义 Native Exception handler 的情况,即通过 MainApplication.java 中 实例化 NativeExceptionHandlerIfc 并重写其 handleNativeException 方法。 if (nativeExceptionHandler != null) { nativeExceptionHandler.handleNativeException(thread, throwable, originalHandler); } else { // 获取 activity 并展示错误信息(一个弹窗,并提供重启和退出按钮) activity = getCurrentActivity(); Intent i = new Intent(); i.setClass(activity, errorIntentTargetClass); i.putExtra("stack_trace_string",stackTraceString); i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); activity.startActivity(i); activity.finish(); // 允许执行且已存在异常处理函数时,执行原异常处理函数 if (executeOriginalUncaughtExceptionHandler && originalHandler != null) { originalHandler.uncaughtException(thread, throwable); } // 设置出现异常情况直接退出 if (forceToQuit) { System.exit(0); } } } }); }

可以看到,主要做了四件事:

实例化 new Thread.UncaughtExceptionHandler(),并重写其 uncaughtException 方法; uncaughtException 方法中执行 JS 回调函数; 兼容自定义 Native Exception handler 的情况; 调用 Thread.setDefaultUncaughtExceptionHandler 重置异常处理器;

1.2 iOS 异常捕获

iOS 通常利用 NSSetUncaughtExceptionHandler 设置全部的异常处理器,当异常情况发生时,会执行其设置的异常处理器。react-native-exception-handler 也是基于此实现对 iOS 端异常的捕获,如下所示:

// ==================================== // REACT NATIVE MODULE EXPOSED METHODS // ==================================== RCT_EXPORT_MODULE(); // METHOD TO INITIALIZE THE EXCEPTION HANDLER AND SET THE JS CALLBACK BLOCK RCT_EXPORT_METHOD(setHandlerforNativeException:(BOOL)callPreviouslyDefinedHandler withCallback: (RCTResponseSenderBlock)callback) { // 1.设置异常处理函数用于执行 JS 回调; jsErrorCallbackBlock = ^(NSException *exception, NSString *readeableException){ callback(@[readeableException]); }; // 2.获取已存在的 native 异常处理器; previousNativeErrorCallbackBlock = NSGetUncaughtExceptionHandler(); callPreviousNativeErrorCallbackBlock = callPreviouslyDefinedHandler; // 3. 利用 NSSetUncaughtExceptionHandler 自定义异常处理器 HandleException; NSSetUncaughtExceptionHandler(&HandleException); signal(SIGABRT, SignalHandler); signal(SIGILL, SignalHandler); signal(SIGSEGV, SignalHandler); signal(SIGFPE, SignalHandler); signal(SIGBUS, SignalHandler); //signal(SIGPIPE, SignalHandler); //Removing SIGPIPE as per https://github.com/master-atul/react-native-exception-handler/issues/32 NSLog(@"REGISTERED RN EXCEPTION HANDLER"); }

上述代码主要做了三件事:

设置异常处理函数用于执行 JS 回调; 获取已存在的 native 异常处理器; 利用 NSSetUncaughtExceptionHandler 自定义异常处理器 HandleException;

接下来,看下具体的 handleException 又做了些什么呢?

// ================================================================ // ACTUAL CUSTOM HANDLER called by the EXCEPTION AND SIGNAL HANDLER // WHICH KEEPS THE APP RUNNING ON EXCEPTION // ================================================================ - (void)handleException:(NSException *)exception { NSString * readeableError = [NSString stringWithFormat:NSLocalizedString(@"%@\n%@", nil), [exception reason], [[exception userInfo] objectForKey:RNUncaughtExceptionHandlerAddressesKey]]; dismissApp = false; // 1.允许执行且已存在异常处理函数时,执行原异常处理函数 if (callPreviousNativeErrorCallbackBlock && previousNativeErrorCallbackBlock) { previousNativeErrorCallbackBlock(exception); } // 2. 用于兼容自定义 Native Exception handler 的情况,可通过调用 replaceNativeExceptionHandlerBlock 实现 if(nativeErrorCallbackBlock != nil){ nativeErrorCallbackBlock(exception,readeableError); }else{ defaultNativeErrorCallbackBlock(exception,readeableError); } // 3. 执行 js 异常处理函数 jsErrorCallbackBlock(exception,readeableError); CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop); while (!dismissApp) { long count = CFArrayGetCount(allModes); long i = 0; while(i < count){ NSString *mode = CFArrayGetValueAtIndex(allModes, i); if(![mode isEqualToString:@"kCFRunLoopCommonModes"]){ CFRunLoopRunInMode((CFStringRef)mode, 0.001, false); } i++; } } CFRelease(allModes); NSSetUncaughtExceptionHandler(NULL); signal(SIGABRT, SIG_DFL); signal(SIGILL, SIG_DFL); signal(SIGSEGV, SIG_DFL); signal(SIGFPE, SIG_DFL); signal(SIGBUS, SIG_DFL); signal(SIGPIPE, SIG_DFL); kill(getpid(), [[[exception userInfo] objectForKey:RNUncaughtExceptionHandlerSignalKey] intValue]); }

1.3 小结

通过对 react-native-exception-handler 源码的解读,可以知道,Android 和 iOS 分别利用 Thread.UncaughtExceptionHandlerNSSetUncaughtExceptionHandler 实现对应用程序的异常捕获。需要注意一点的是,当我们重置异常处理器时,需要考虑到其已存在的异常处理逻辑,避免将其直接覆盖,导致其他监测处理程序失效。

2. React 异常捕获

为了解决部分 UI 的 JavaScript 错误导致整个应用白屏或者崩溃的问题,React 16 引入了新的概念 —— Error Boundaries(错误边界)。

错误边界是一种 React 组件,这种组件 可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。

借用 static getDerivedStateFromError()componentDidCatch() 两个生命周期实现错误边界,当抛出错误后,使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。

class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // 更新 state 使下一次渲染能够显示降级后的 UI return { hasError: true }; } componentDidCatch(error, errorInfo) { // 你同样可以将错误日志上报给服务器 logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // 你可以自定义降级后的 UI 并渲染 return <Text>Something went wrong.</Text>; } return this.props.children; } }

错误边界仅可以捕获其子组件的错误,它无法捕获其自身的错误。错误边界无法捕获以下场景中产生的错误:

事件处理 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数) 服务端渲染 它自身抛出来的错误(并非它的子组件)

3. JS 异常捕获

上文中提到,Error Boundaries 能捕获子组件生命周期函数中的异常,包括构造函数(constructor)和 render 函数。而无法捕获以下异常:

事件处理 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数) 服务端渲染 它自身抛出来的错误(并非它的子组件)

对于这些错误边界无法捕获的异常,在 web 中可以通过 window.onerror() 加载一个全局的error事件处理函数用于自动收集错误报告。

那么 React Native 中是如何处理的呢?

3.1 BatchedBridge

React Native 是通过 JS Bridge 处理 JS 与 Native 的所有通信的,而 JS Bridge (BatchedBridge.js)是 MessageQueue.js 的实例。

'use strict'; const MessageQueue = require('MessageQueue'); const BatchedBridge = new MessageQueue(); Object.defineProperty(global, '__fbBatchedBridge', { configurable: true, value: BatchedBridge, }); module.exports = BatchedBridge;

BatchedBridge 创建一个 MessageQueue 实例,并将它定义到全局变量中,以便给 JSCExecutor.cpp 中获取到。

3.2 MessageQueue

MessageQueue 是 JS Context 和 Native Context 之间的唯一连接,如图,网络请求/响应、布局计算、渲染请求、用户交互、动画序列指令、Native 模块的调用和 I/O 的操作等,都要经过 MessageQueue 进行处理。开发中,可以通过调用 MessageQueue.spy 查看 JS <-> Native 之间的具体通信过程:

import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue'; MessageQueue.spy(true); // -or- // MessageQueue.spy((info) => console.log("I'm spying!", info));

MessageQueue.js 有三个作用:

注册所有的 JavaScriptModule 提供方法供 c++ 端调用 分发 js 端 NativeModule 所有异步方法的调用(同步方法会直接调用c++端代码)

查看 MessageQueue.js 构造函数:

constructor() { this._lazyCallableModules = {}; this._queue = [[], [], [], 0]; this._successCallbacks = []; this._failureCallbacks = []; this._callID = 0; this._lastFlush = 0; this._eventLoopStartTime = new Date().getTime(); if (__DEV__) { this._debugInfo = {}; this._remoteModuleTable = {}; this._remoteMethodTable = {}; } (this: any).callFunctionReturnFlushedQueue = this.callFunctionReturnFlushedQueue.bind( this, ); (this: any).callFunctionReturnResultAndFlushedQueue = this.callFunctionReturnResultAndFlushedQueue.bind( this, ); (this: any).flushedQueue = this.flushedQueue.bind(this); (this: any).invokeCallbackAndReturnFlushedQueue = this.invokeCallbackAndReturnFlushedQueue.bind( this, ); }

从 MessageQueue 源码中,可以看到,其定义了多个变量以及四个函数:

callFunctionReturnFlushedQueue callFunctionReturnResultAndFlushedQueue flushedQueue invokeCallbackAndReturnFlushedQueue

而以上四个函数的调用时机则是交给 c++ 端 NativeToJsBridge.cpp,具体的通信机制可参考文章

继续阅读上述四个函数的实现,可以看到都调用了 MessageQueue 的私有方法 __guard:

__guard(fn: () => void) { if (this.__shouldPauseOnThrow()) { fn(); } else { try { fn(); } catch (error) { ErrorUtils.reportFatalError(error); } } }

代码很简单,可以看到 __guard 会 根据 \___shouldPauseOnThrow 的返回值决定是否对 fn 进行 try catch 处理,当 __shouldPauseOnThrow 返回 false 时,且 fn 有异常时,则会执行 ErrorUtils.reportFatalError(error) 将错误上报。

// MessageQueue installs a global handler to catch all exceptions where JS users can register their own behavior // This handler makes all exceptions to be propagated from inside MessageQueue rather than by the VM at their origin // This makes stacktraces to be placed at MessageQueue rather than at where they were launched // The parameter DebuggerInternal.shouldPauseOnThrow is used to check before catching all exceptions and // can be configured by the VM or any Inspector __shouldPauseOnThrow(): boolean { return ( // $FlowFixMe typeof DebuggerInternal !== 'undefined' && DebuggerInternal.shouldPauseOnThrow === true // eslint-disable-line no-undef ); }

注释写的也很清晰,MessageQueue 设置了一个用于处理所有 JS 侧异常行为的处理器,并且可以通过设置 DebuggerInternal.shouldPauseOnThrow 来决定是否对异常进行捕获。

3.3 ErrorUtils

/** * This is the error handler that is called when we encounter an exception * when loading a module. This will report any errors encountered before * ExceptionsManager is configured. */ let _globalHandler: ErrorHandler = function onError( e: mixed, isFatal: boolean, ) { throw e; }; /** * The particular require runtime that we are using looks for a global * `ErrorUtils` object and if it exists, then it requires modules with the * error handler specified via ErrorUtils.setGlobalHandler by calling the * require function with applyWithGuard. Since the require module is loaded * before any of the modules, this ErrorUtils must be defined (and the handler * set) globally before requiring anything. */ const ErrorUtils = { setGlobalHandler(fun: ErrorHandler): void { _globalHandler = fun; }, getGlobalHandler(): ErrorHandler { return _globalHandler; }, reportError(error: mixed): void { _globalHandler && _globalHandler(error, false); }, reportFatalError(error: mixed): void { // NOTE: This has an untyped call site in Metro. _globalHandler && _globalHandler(error, true); }, ... }

当调用 ErrorUtils.reportFatalError(error) 时,若存在 __globalHandler 则执行 _globalHandler,并将错误信息作为参数传入。同时,ErrorUtils 提供了函数 setGlobalHandler 用于重置 _globalHandler。

global.ErrorUtils.setGlobalHandler(function (err) { consolo.log('global error: ', err); });

3.4 demo

那么 JS 的异常错误会被 MessageQueue 处理吗?我们可以开启 MessageQueue 看下其日志。

import React from 'react'; import { View, Text, } from 'react-native'; import MessageQueue from 'react-native/Libraries/BatchedBridge/MessageQueue'; MessageQueue.spy(true); // -or- MessageQueue.spy((info) => console.log("I'm spying!", info)); global.ErrorUtils.setGlobalHandler(function (err) { consolo.log('global error: ', err); }); const App = () => { const onPressButton = () => { throw new Error('i am error'); }; return ( <View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}> <Text onPress={onPressButton}>按钮</Text> </View> ); };

当点击屏幕按钮时,可在控制台上看到如下信息:

可以看到,当 JS 抛出异常时,会被 ErrorUtils 捕获到,并执行通过 global.ErrorUtils.setGlobalHandler 设置的处理函数。

注意:0.64 版本开始,react-native pollfills 相关(包含 ErrorUtils 实现)已由 react-native/Libraries/polyfills 抽离为 @react-native/polyfills

4 Promise 异常捕获

除了上述提到的几种导致 APP crash 或者崩溃的异常处理之外,当我们使用 Promise 时,若抛出异常时未被 catch 捕获或在 catch 阶段再次抛出异常,此时会导致后续逻辑无法正常执行。

在 web 端,浏览器会自动追踪内存使用情况,通过垃圾回收机制处理这个 rejected Promise,并且提供unhandledrejection事件进行监听。

window.addEventListener('unhandledrejection', event => ···);

那么,那么在 React Native 中是如何处理此类 Promise 异常的呢?

在 RN 中,当遇到未处理的 Promise 异常时,控制台输出黄色警告⚠️:

而设备则表现为弹出黄屏:

<div align="center">

<img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b2415c6a36a5463f862879a3303fe416~tplv-k3u1fbpfcp-watermark.image" />

</div>

查看源码 react-native/Libraries/Promise.js 可知,RN 默认在开发环境下,通过promise/setimmediate/rejection-tracking去追踪 rejected 状态的Promise,并提供了onUnhandled回调函数处理未进行处理的 rejected Promise:

if (__DEV__) { require('promise/setimmediate/rejection-tracking').enable({ allRejections: true, onUnhandled: (id, error) => { const {message, stack} = error; const warning = `Possible Unhandled Promise Rejection (id: ${id}):\n` + (message == null ? '' : `${message}\n`) + (stack == null ? '' : stack); console.warn(warning); }, onHandled: (id) => { const warning = `Promise Rejection Handled (id: ${id})\n` + 'This means you can ignore any previous messages of the form ' + `"Possible Unhandled Promise Rejection (id: ${id}):"`; console.warn(warning); }, }); }

其执行时机可以在rejection-tracking.js中源码中找到:

//... timeout: setTimeout( onUnhandled.bind(null, promise._51), // For reference errors and type errors, this almost always // means the programmer made a mistake, so log them after just // 100ms // otherwise, wait 2 seconds to see if they get handled matchWhitelist(err, DEFAULT_WHITELIST) ? 100 : 2000 ), //...

那么,我们是否可以仿照 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 篇【全网最全,值得收藏】

参考文章

React Native 原理与实践 【从源码分析】可能是全网最实用的React Native异常解决方案 ReactNative 通信机制_c++端源码分析