最近在做 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.UncaughtExceptionHandler
和 NSSetUncaughtExceptionHandler
实现对应用程序的异样捕捉。须要留神一点的是,当咱们重置异样处理器时,须要思考到其已存在的异样解决逻辑,防止将其间接笼罩,导致其余监测处理程序生效。
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++端源码剖析
发表回复