今天小编给大家分享一下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; }
标签: android