今天小编给大家分享一下ReactNative错误采集原理在Android中如何实现的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。
1 JS错误
1.1 Error
Error是错误基类,其他错误继承自Error,Error对象有两个主要属性,name和message
new Error(message)
1.2 常见的错误
SyntaxError:语法错误
语法错误是一种常见的错误,在所有编程语言中都存在,表示不符合编程语言规范。
一类是词法、语法分析转换生成语法树时发生,此类异常一旦发生,导致整个js文件无法执行,而其他异常发生在代码运行时,在错误出现的那一行之前的代码不受影响
const 1xx; // SyntaxError
另一类是运行中出现的语法错误,如开发中常见的json解析错误,参数传入非标准json字符
JSON.parse('') // SyntaxError: Unexpected end of JSON input
ReferenceError:引用错误
引用了一个不能存在的变量,变量未声明就引用了
const a = xxx; // ReferenceError: xxx is not defined
TypeError:类型错误
变量或参数不是有效类型
1() // TypeError: 1 is not a function
const a = new 111() // TypeError: 111 is not a constructor
RangeError:边界错误
超出有效范围时发生异常,常见的是数组长度超出范围
[].length = -1 // RangeError: Invalid array length
URIError:URI错误
调用URI相关函数中出现,包括encodeURI、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()
decodeURI('%') // URIError: URI malformed
1.3 自定义错误
我们可以继承Error类,实现自定义的错误
class MyError extends Error {
constructor(message) {
super(message);
this.name = 'MyError';
}
}
function() {
throw new MyError('error message'); // MyError: error message
}
2 RN错误处理
RN错误处理包括JS和native两部分,由JS捕获,抛给Native处理
2.1 JS部分
2.1.1 MessageQueue
Native和JS通信的消息队列, 负责Native和JS通讯, 包括渲染、交互、各种互相调用等。所有的通信都会经过_guard函数处理,在_guard中会被try-catch住,出现异常后调用ErrorUtils处理
__guard(fn: () => void) {
if (this.__shouldPauseOnThrow()) {
fn();
} else {
try {
fn();
} catch (error) {
ErrorUtils.reportFatalError(error); // 捕获异常,交给ErrorUtils
}
}
}
注:react-native/Libraries/BatchedBridge/MessageQueue.js
2.1.2 ErrorUtils
ErrorUtils用于处理RN中所有的异常,它对暴露异常处理拦截接口
异常上报
收到异常后调用_globalHandler处理异常
// 处理非fatal异常
reportError(error: mixed): void {
_globalHandler && _globalHandler(error, false);
},
// 处理fatal异常
reportFatalError(error: mixed): void {
_globalHandler && _globalHandler(error, true);
},
异常处理
所有异常通过_globalHandle函数处理,默认情况下_globalHandler会直接将错误抛出,ErrorUtils对外提供了setGlobalHanlder做错误拦截处理,RN重写_globalHandler来做错误收集和处理
let _globalHandler: ErrorHandler = function onError(
e: mixed,
isFatal: boolean,
) {
throw e;
};
setGlobalHandler(fun: ErrorHandler): void {
_globalHandler = fun;
},
getGlobalHandler(): ErrorHandler {
return _globalHandler;
},
注:react-native/Libraries/polyfills/error-guard.js
2.1.3 ExceptionsManager
ExceptionsManager是RN中异常管理模块,负责红屏处理、console.error、并将异常传给Native侧
异常处理器设置
调用ErrorUtils.setGlobalHandler,把错误处理实现交给ExceptionsManager.handleException
console.error处理:调用ExceptionsManager.installConsoleErrorReporter重写console.error
const ExceptionsManager = require('./ExceptionsManager');
// Set up console.error handler
ExceptionsManager.installConsoleErrorReporter();
// Set up error handler
if (!global.__fbDisableExceptionsManager) {
const handleError = (e, isFatal) => {
try {
ExceptionsManager.handleException(e, isFatal);
} catch (ee) {
console.log('Failed to print error: ', ee.message);
throw e;
}
};
const ErrorUtils = require('../vendor/core/ErrorUtils');
ErrorUtils.setGlobalHandler(handleError);
}
注:react-native/Libraries/Core/setUpErrorHandling.js
ExceptionsManager处理异常
构建Error:如果错误不是Error类型,构造一个SyntheticError,方便日志输出和展示
function handleException(e: mixed, isFatal: boolean) {
let error: Error;
if (e instanceof Error) {
error = e;
} else {
error = new SyntheticError(e);
}
reportException(error, isFatal);
}
调用错误处理
function reportException(e: ExtendedError, isFatal: boolean) {
const NativeExceptionsManager = require('./NativeExceptionsManager').default;
if (NativeExceptionsManager) {
// 解析错误,获取错误信息、堆栈
const parseErrorStack = require('./Devtools/parseErrorStack');
const stack = parseErrorStack(e);
const currentExceptionID = ++exceptionID;
const originalMessage = e.message || '';
let message = originalMessage;
if (e.componentStack != null) {
message += `
This error is located at:${e.componentStack}`;
}
const namePrefix = e.name == null || e.name === '' ? '' : `${e.name}: `;
const isFromConsoleError = e.name === 'console.error';
if (!message.startsWith(namePrefix)) {
message = namePrefix + message;
}
// 如果是console.error则输出
if (!isFromConsoleError) {
if (console._errorOriginal) {
console._errorOriginal(message);
} else {
console.error(message);
}
}
message =
e.jsEngine == null ? message : `${message}, js engine: ${e.jsEngine}`;
// 抑制(不展示)红屏,不展示native红屏弹窗,forceRedbox默认为false
const isHandledByLogBox =
e.forceRedbox !== true && global.__unstable_isLogBoxEnabled === true;
const data = preprocessException({
message,
originalMessage: message === originalMessage ? null : originalMessage,
name: e.name == null || e.name === '' ? null : e.name,
componentStack:
typeof e.componentStack === 'string' ? e.componentStack : null,
stack,
id: currentExceptionID,
isFatal,
extraData: {
jsEngine: e.jsEngine,
rawStack: e.stack,
// Hack to hide native redboxes when in the LogBox experiment.
// This is intentionally untyped and stuffed here, because it is temporary.
suppressRedBox: isHandledByLogBox,
},
});
// 如果抑制native红屏,展示JS红屏提示错误
if (isHandledByLogBox) {
LogBoxData.addException({
...data,
isComponentError: !!e.isComponentError,
});
}
// 把调用NativeExceptionsManager上报给native
NativeExceptionsManager.reportException(data);
}
}
NativeExceptionsManager调用native模块上报错误
// Native导出类,以Android为例,对应ExceptionsManagerModule.java
const NativeModule = TurboModuleRegistry.getEnforcing<Spec>(
'ExceptionsManager',
);
const ExceptionsManager{
// 判断是否是fatal调用不同函数上报
reportException(data: ExceptionData): void {
if (data.isFatal) {
ExceptionsManager.reportFatalException(data.message, data.stack, data.id);
} else {
ExceptionsManager.reportSoftException(data.message, data.stack, data.id);
}
},
// 上报fatal异常
reportFatalException(
message: string,
stack: Array<StackFrame>,
exceptionId: number,
) {
NativeModule.reportFatalException(message, stack, exceptionId);
},
// 上报soft异常
reportSoftException(
message: string,
stack: Array<StackFrame>,
exceptionId: number,
) {
NativeModule.reportSoftException(message, stack, exceptionId);
},
// Android提供关闭红屏函数
dismissRedbox(): void {
if (Platform.OS !== 'ios' && NativeModule.dismissRedbox) {
// TODO(T53311281): This is a noop on iOS now. Implement it.
NativeModule.dismissRedbox();
}
},
}
console.error处理
上述提到调用ExceptionsManager.installConsoleErrorReporter处理console.error,处理成非fatal异常
function installConsoleErrorReporter() {
// 如果设置过,return
if (console._errorOriginal) {
return; // already installed
}
console._errorOriginal = console.error.bind(console);
// 设置console.error处理函数
console.error = reactConsoleErrorHandler;
if (console.reportErrorsAsExceptions === undefined) {
console.reportErrorsAsExceptions = true;
}
}
// console.error处理函数,最终调用reportException上报成非fatal异常
function reactConsoleErrorHandler() {
if (arguments[0] && arguments[0].stack) {
// 上报
reportException(arguments[0], /* isFatal */ false);
} else {
// 构造一个SyntheticError
const stringifySafe = require('../Utilities/stringifySafe');
const str = Array.prototype.map
.call(arguments, value =>
typeof value === 'string' ? value : stringifySafe(value),
)
.join(' ');
const error: ExtendedError = new SyntheticError(str);
error.name = 'console.error';
// 上报
reportException(error, /* isFatal */ false);
}
}
注:react-native/Libraries/Core/ExceptionsManager.js
注:跟进上述源码可知,红屏是通过isHandledByLogBox参数可以禁止native红屏弹窗,isHandledByLogBox是通过global.__unstable_isLogBoxEnabled控制,可以通过下面方式禁止native红屏展示,但是还是会展示js红屏来提示错误
global.__unstable_isLogBoxEnabled = true;
YellowBox.__unstable_enableLogBox(); // 内部调用了上面的代码
2.2 Native部分
2.2.1 ExceptionsManagerModule
上面讲述了JS处理异常后将异常抛给native处理,ExceptionsManagerModule是native处理异常模块,导出给JS类名为ExceptionsManager
ExceptionsManagerModule异常处理
// 上报fatal异常
@ReactMethod
public void reportFatalException(String message, ReadableArray stack, int id) {
JavaOnlyMap data = new JavaOnlyMap();
data.putString("message", message);
data.putArray("stack", stack);
data.putInt("id", id);
data.putBoolean("isFatal", true);
reportException(data);
}
// 上报soft异常
@ReactMethod
public void reportSoftException(String message, ReadableArray stack, int id) {
JavaOnlyMap data = new JavaOnlyMap();
data.putString("message", message);
data.putArray("stack", stack);
data.putInt("id", id);
data.putBoolean("isFatal", false);
reportException(data);
}
// 最终调用reportException
@ReactMethod
public void reportException(ReadableMap data) {
// 错误堆栈
String message = data.hasKey("message") ? data.getString("message") : "";
ReadableArray stack = data.hasKey("stack") ? data.getArray("stack") : Arguments.createArray();
int id = data.hasKey("id") ? data.getInt("id") : -1;
boolean isFatal = data.hasKey("isFatal") ? data.getBoolean("isFatal") : false;
// dev模式,展示红屏dialog
if (mDevSupportManager.getDevSupportEnabled()) {
// 获取是否抑制红屏参数,对应js侧传入的isHandledByLogBox
boolean suppressRedBox = false;
if (data.getMap("extraData") != null && data.getMap("extraData").hasKey("suppressRedBox")) {
suppressRedBox = data.getMap("extraData").getBoolean("suppressRedBox");
}
if (!suppressRedBox) {
mDevSupportManager.showNewJSError(message, stack, id); // 显示红屏弹窗
}
} else {
// fatal抛出JavascriptException异常,非fatal打印出来
if (isFatal) {
throw new JavascriptException(jsStackTrace)
.setExtraDataAsJson(extraDataAsJson);
} else {
logException(jsStackTrace, extraDataAsJson);
}
}
}
@ReactMethod
public void dismissRedbox() {
if (mDevSupportManager.getDevSupportEnabled()) {
mDevSupportManager.hideRedboxDialog();
}
}
// 上报soft异常
- (void)reportSoft: (NSString *)message stack:(NSArray<NSDictionary *> *)stack exceptionId:(double)exceptionId suppressRedBox: (BOOL) suppressRedBox {
if (!suppressRedBox) {
[_bridge.redBox showErrorMessage:message withStack:stack errorCookie:((int)exceptionId)];
}
if (_delegate) {
[_delegate handleSoftJSExceptionWithMessage:message stack:stack exceptionId:[NSNumber numberWithDouble:exceptionId]];
}
}
// 上报fatal异常
- (void)reportFatal: (NSString *)message stack:(NSArray<NSDictionary *> *)stack exceptionId:(double)exceptionId suppressRedBox: (BOOL) suppressRedBox {
if (!suppressRedBox) {
[_bridge.redBox showErrorMessage:message withStack:stack errorCookie:((int)exceptionId)];
}
if (_delegate) {
[_delegate handleFatalJSExceptionWithMessage:message stack:stack exceptionId:[NSNumber numberWithDouble:exceptionId]];
}
static NSUInteger reloadRetries = 0;
if (!RCT_DEBUG && reloadRetries < _maxReloadAttempts) {
reloadRetries++;
RCTTriggerReloadCommandListeners(@"JS Crash Reload");
} else if (!RCT_DEV || !suppressRedBox) {
NSString *description = [@"Unhandled JS Exception: " stringByAppendingString:message];
NSDictionary *errorInfo = @{ NSLocalizedDescriptionKey: description, RCTJSStackTraceKey: stack };
RCTFatal([NSError errorWithDomain:RCTErrorDomain code:0 userInfo:errorInfo]);
}
}
// reportException
RCT_EXPORT_METHOD(reportException:(JS::NativeExceptionsManager::ExceptionData &)data)
{
NSString *message = data.message();
double exceptionId = data.id_();
id<NSObject> extraData = data.extraData();
// Reserialize data.stack() into an array of untyped dictionaries.
// TODO: (moti) T53588496 Replace `(NSArray<NSDictionary *> *)stack` in
// reportFatalException etc with a typed interface.
NSMutableArray<NSDictionary *> *stackArray = [NSMutableArray<NSDictionary *> new];
for (auto frame: data.stack()) {
NSMutableDictionary * frameDict = [NSMutableDictionary new];
if (frame.column().hasValue()) {
frameDict[@"column"] = @(frame.column().value());
}
frameDict[@"file"] = frame.file();
if (frame.lineNumber().hasValue()) {
frameDict[@"lineNumber"] = @(frame.lineNumber().value());
}
frameDict[@"methodName"] = frame.methodName();
if (frame.collapse().hasValue()) {
frameDict[@"collapse"] = @(frame.collapse().value());
}
[stackArray addObject:frameDict];
}
NSDictionary *dict = (NSDictionary *)extraData;
BOOL suppressRedBox = [[dict objectForKey:@"suppressRedBox"] boolValue];
if (data.isFatal()) {
[self reportFatal:message stack:stackArray exceptionId:exceptionId suppressRedBox:suppressRedBox];
} else {
[self reportSoft:message stack:stackArray exceptionId:exceptionId suppressRedBox:suppressRedBox];
}
}
问题:fatal错误抛出异常后为什么应用为什么没有退出呢?
DevSupportManager处理红屏
@Override
public void showNewJavaError(@Nullable String message, Throwable e) {
FLog.e(ReactConstants.TAG, "Exception in native call", e);
showNewError(
message, StackTraceHelper.convertJavaStackTrace(e), JAVA_ERROR_COOKIE, ErrorType.NATIVE);
}
// 展示红屏弹窗
private void showNewError(
@Nullable final String message,
final StackFrame[] stack,
final int errorCookie,
final ErrorType errorType) {
UiThreadUtil.runOnUiThread(
new Runnable() {
@Override
public void run() {
if (mRedBoxDialog == null) {
Activity context = mReactInstanceManagerHelper.getCurrentActivity();
mRedBoxDialog = new RedBoxDialog(context, DevSupportManagerImpl.this, mRedBoxHandler);
}
if (mRedBoxDialog.isShowing()) {
return;
}
Pair<String, StackFrame[]> errorInfo = processErrorCustomizers(Pair.create(message, stack));
mRedBoxDialog.setExceptionDetails(errorInfo.first, errorInfo.second);
mRedBoxDialog.resetReporting();
mRedBoxDialog.show();
}
});
}
2.2.2 线程异常捕获(Android)
Handle捕获异常
RN引擎创建的时候会初始化三个线程,UiThread、NativeModulesThread、JSThread,这些线程通过MessageQueueThreadHandler处理消息队列,MessageQueueThreadHandler重写了Handle的dispatchMessage函数,函数通过try-catch包裹防止应用直接退出,出现异常时调用QueueThreadExceptionHandler处理(引擎实现此接口),这里能拦截所有的异常,包括上述js捕获传到native手动抛出的、yoga布局过程中的等等
public class MessageQueueThreadHandler extends Handler {
private final QueueThreadExceptionHandler mExceptionHandler;
public MessageQueueThreadHandler(Looper looper, QueueThreadExceptionHandler exceptionHandler) {
super(looper);
mExceptionHandler = exceptionHandler;
}
@Override
public void dispatchMessage(Message msg) {
try {
super.dispatchMessage(msg);
} catch (Exception e) {
mExceptionHandler.handleException(e);
}
}
}
引擎处理异常
在引擎(CatalystInstanceImpl)的内部类NativeExceptionHandler中,实现了QueueThreadExceptionHandler接口,在引擎创建时初始化,出现异常时调用NativeModuleCallExceptionHandler处理,并销毁引擎
// 内部类实现QueueThreadExceptionHandler,叫异常交给引擎的onNativeException处理
private static class NativeExceptionHandler implements QueueThreadExceptionHandler {
@Override
public void handleException(Exception e) {
if (ReactFeatureFlags.enableCatalystCleanupFix) {
CatalystInstanceImpl catalystInstance = mCatalystInstanceImplWeak.get();
if (catalystInstance != null) {
catalystInstance.onNativeException(e);
}
} else {
mCatalystInstanceImpl.onNativeException(e);
}
}
}
// 调用NativeModuleCallExceptionHandler处理异常,并销毁引擎
private void onNativeException(Exception e) {
mHasNativeError.set(true);
boolean isAlive = !mDestroyed;
if (isAlive) {
mNativeModuleCallExceptionHandler.handleException(e);
}
mReactQueueConfiguration
.getUIQueueThread()
.runOnQueue(
new Runnable() {
@Override
public void run() {
// 销毁引擎
destroy(() -> {
if (mDestroyFinishedCallback != null) {
mDestroyFinishedCallback.onDestroyFinished();
mDestroyFinishedCallback = null;
}
});
}
});
}
注:com.facebook.react.bridge.CatalystInstanceImpl(引擎实现类)
2.2.3 最终的异常处理
默认处理方式
上述讲到引擎捕获异常后会调用NativeModuleCallExceptionHandler.handleException处理,它是个接口,引擎提供了默认实现类,默认实现类收到异常后是直接抛出,会导致应用退出
public interface NativeModuleCallExceptionHandler {
/** Do something to display or log the exception. */
void handleException(Exception e);
void handleCaughtException(Exception e);
}
// 默认实现类
public class DefaultNativeModuleCallExceptionHandler implements NativeModuleCallExceptionHandler {
@Override
public void handleException(Exception e) {
if (e instanceof RuntimeException) {
// Because we are rethrowing the original exception, the original stacktrace will be
// preserved.
throw (RuntimeException) e;
} else {
throw new RuntimeException(e);
}
}
@Override
public void handleCaughtException(Exception e) {
e.printStackTrace();
}
}
自定义异常处理
为了防止默认处理方式将异常直接抛出导致crash,业务可以实现自定义的NativeModuleCallExceptionHandler接口来处理异常,将异常上报,并展示错误兜底页面
3 整体流程
基于上述源码解析可知,RN错误采集流程由JS侧中MessageQueue发起,经过一系列处理和封装,传到native侧,再经过native一系列转发,最终交给由引擎(CatalyInstanceImple)处理,整体流程如下图所示
4 错误兜底
页面出现异常后,对异常状态兜底是一种保障线上质量的常规手段。当页面发生严重 JS 错误(FatalError)时,会展示错误页面无法继续使用。这种方式在一些业务场景下并不友好。比如:页面上某一个次要模块发生异常,并不影响核心功能的使用,这种情况下展示出错页面有些不必要
React 16 中引入了一个新概念——错误边界(Error Boundaries)。错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界能在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误
基于这个特性,业务能够自定义控制接收到JSError的行为,能更优雅地处理错误兜底及展示
4.1 什么是错误边界
4.1.1 概念
错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JS 错误,并且它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界能在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误
4.1.2 错误边界的关键模块
错误边界是通过 try-catch 方式捕获异常的,它在哪里进行捕获异常的呢?React 有三个重要组成模块,错误边界在 Reconciliation 中对异常进行捕获。
React基础模块(这个模块定义了React的基础API及组件相关内容。对应我们开发页面时引入的 'react' 模块)
渲染模块(这个模块对于不同类型的应用,采用不同的渲染方式。对应我们开发页面时引入的 'react-dom' 模块)
Reconciliation 模块(又叫“协调模块”,这个模块是上面两个模块的基础,主要负责任务协调、生命周期函数管理等)
4.1.3 Reconciliation介绍
Reconciliation模块是React三个重要模块之一,又叫“协调模块”,这个模块是上面两个模块的基础,主要负责任务协调、生命周期函数管理等,它分为render和commit两个阶段
render阶段:简单来说就是找到需要更新的工作,通过 Diff Fiber Tree 找出要做的更新工作,这是一个js计算过程,计算结果可以被缓存,计算过程可以被打断,也可以恢复执行。
commit阶段:提交更新并调用对应渲染模块(react-dom)进行渲染,为了防止页面抖动,该过程是同步且不能被打断
// Reconciliation阶段开始,render阶段,performSyncWorkOnRoot(同步更新)、performConcurrentWorkOnRoot(异步)
function performSyncWorkOnRoot(root) {
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
}
function handleError(root, thrownValue) {
do {
try {
throwException(
root,
workInProgress.return,
workInProgress,
thrownValue,
renderExpirationTime
);
workInProgress = completeUnitOfWork(workInProgress);
} catch (yetAnotherThrownValue)
thrownValue = yetAnotherThrownValue;
continue;
} // Return to the normal work loop.
return;
} while (true);
}
function throwException(
root,
returnFiber,
sourceFiber,
value,
renderExpirationTime
) {
case ClassComponent:
var _update2 = createClassErrorUpdate(
workInProgress,
errorInfo,
renderExpirationTime
);
enqueueCapturedUpdate(workInProgress, _update2);
return;
}
}
function createClassErrorUpdate(fiber, errorInfo, expirationTime) {
var update = createUpdate(expirationTime, null);
update.tag = CaptureUpdate;
var getDerivedStateFromError = fiber.type.getDerivedStateFromError;
if (typeof getDerivedStateFromError === "function") {
var error = errorInfo.value;
update.payload = function() {
logError(fiber, errorInfo);
return getDerivedStateFromError(error);
};
}
var inst = fiber.stateNode;
if (inst !== null && typeof inst.componentDidCatch === "function") {
update.callback = function callback() {
{
markFailedErrorBoundaryForHotReloading(fiber);
}
if (typeof getDerivedStateFromError !== "function") {
markLegacyErrorBoundaryAsFailed(this); // Only log here if componentDidCatch is the only error boundary method defined
logError(fiber, errorInfo);
}
var error = errorInfo.value;
var stack = errorInfo.stack;
this.componentDidCatch(error, {
componentStack: stack !== null ? stack : ""
});
{
if (typeof getDerivedStateFromError !== "function") {
!(fiber.expirationTime === Sync)
? warningWithoutStack$1(
false,
"%s: Error boundaries should implement getDerivedStateFromError(). " +
"In that method, return a state update to display an error message or fallback UI.",
getComponentName(fiber.type) || "Unknown"
)
: void 0;
}
}
};
} else {
update.callback = function() {
markFailedErrorBoundaryForHotReloading(fiber);
};
}
return update;
}