ReactNative錯(cuò)誤采集原理在Android中實(shí)現(xiàn)詳解
引言
當(dāng)我們開發(fā)程序的時(shí),經(jīng)常會(huì)出現(xiàn)一些錯(cuò)誤情況,我們通常用Error類來反映錯(cuò)誤內(nèi)容,如網(wǎng)絡(luò)中有HttpError,數(shù)據(jù)庫中DbError等等,這些錯(cuò)誤的采集和分析對(duì)程序改進(jìn)尤為重要
在ReactNative(后續(xù)簡稱RN)中也有錯(cuò)誤,RN是基于React開發(fā)的跨平臺(tái)開發(fā)框架,代碼采用JS實(shí)現(xiàn),所以本文在介紹RN錯(cuò)誤采集之前先介紹JS中常見的錯(cuò)誤,再通過源碼來分析RN錯(cuò)誤收集原理,最后介紹RN錯(cuò)誤兜底方案,為后續(xù)RN在業(yè)務(wù)實(shí)踐中做好理論基礎(chǔ),幫助改善程序穩(wěn)定性
1 JS錯(cuò)誤
1.1 Error
Error是錯(cuò)誤基類,其他錯(cuò)誤繼承自Error,Error對(duì)象有兩個(gè)主要屬性,name和message
new Error(message)
1.2 常見的錯(cuò)誤
SyntaxError:語法錯(cuò)誤
語法錯(cuò)誤是一種常見的錯(cuò)誤,在所有編程語言中都存在,表示不符合編程語言規(guī)范。
一類是詞法、語法分析轉(zhuǎn)換生成語法樹時(shí)發(fā)生,此類異常一旦發(fā)生,導(dǎo)致整個(gè)js文件無法執(zhí)行,而其他異常發(fā)生在代碼運(yùn)行時(shí),在錯(cuò)誤出現(xiàn)的那一行之前的代碼不受影響
const 1xx; // SyntaxError
另一類是運(yùn)行中出現(xiàn)的語法錯(cuò)誤,如開發(fā)中常見的json解析錯(cuò)誤,參數(shù)傳入非標(biāo)準(zhǔn)json字符
JSON.parse('') // SyntaxError: Unexpected end of JSON input
ReferenceError:引用錯(cuò)誤
引用了一個(gè)不能存在的變量,變量未聲明就引用了
const a = xxx; // ReferenceError: xxx is not defined
TypeError:類型錯(cuò)誤
變量或參數(shù)不是有效類型
1() // TypeError: 1 is not a function const a = new 111() // TypeError: 111 is not a constructor
RangeError:邊界錯(cuò)誤
超出有效范圍時(shí)發(fā)生異常,常見的是數(shù)組長度超出范圍
[].length = -1 // RangeError: Invalid array length
URIError:URI錯(cuò)誤
調(diào)用URI相關(guān)函數(shù)中出現(xiàn),包括encodeURI、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()
decodeURI('%') // URIError: URI malformed
1.3 自定義錯(cuò)誤
我們可以繼承Error類,實(shí)現(xiàn)自定義的錯(cuò)誤
class MyError extends Error {
constructor(message) {
super(message);
this.name = 'MyError';
}
}
function() {
throw new MyError('error message'); // MyError: error message
}
2 RN錯(cuò)誤處理
RN錯(cuò)誤處理包括JS和native兩部分,由JS捕獲,拋給Native處理
2.1 JS部分
2.1.1 MessageQueue
Native和JS通信的消息隊(duì)列, 負(fù)責(zé)Native和JS通訊, 包括渲染、交互、各種互相調(diào)用等。所有的通信都會(huì)經(jīng)過_guard函數(shù)處理,在_guard中會(huì)被try-catch住,出現(xiàn)異常后調(diào)用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中所有的異常,它對(duì)暴露異常處理攔截接口
- 異常上報(bào)
收到異常后調(diào)用_globalHandler處理異常
// 處理非fatal異常
reportError(error: mixed): void {
_globalHandler && _globalHandler(error, false);
},
// 處理fatal異常
reportFatalError(error: mixed): void {
_globalHandler && _globalHandler(error, true);
},
- 異常處理
所有異常通過_globalHandle函數(shù)處理,默認(rèn)情況下_globalHandler會(huì)直接將錯(cuò)誤拋出,ErrorUtils對(duì)外提供了setGlobalHanlder做錯(cuò)誤攔截處理,RN重寫_globalHandler來做錯(cuò)誤收集和處理
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中異常管理模塊,負(fù)責(zé)紅屏處理、console.error、并將異常傳給Native側(cè)
異常處理器設(shè)置
- 調(diào)用ErrorUtils.setGlobalHandler,把錯(cuò)誤處理實(shí)現(xiàn)交給ExceptionsManager.handleException
- console.error處理:調(diào)用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處理異常
- 構(gòu)建Error:如果錯(cuò)誤不是Error類型,構(gòu)造一個(gè)SyntheticError,方便日志輸出和展示
function handleException(e: mixed, isFatal: boolean) {
let error: Error;
if (e instanceof Error) {
error = e;
} else {
error = new SyntheticError(e);
}
reportException(error, isFatal);
}
- 調(diào)用錯(cuò)誤處理
function reportException(e: ExtendedError, isFatal: boolean) {
const NativeExceptionsManager = require('./NativeExceptionsManager').default;
if (NativeExceptionsManager) {
// 解析錯(cuò)誤,獲取錯(cuò)誤信息、堆棧
const parseErrorStack = require('./Devtools/parseErrorStack');
const stack = parseErrorStack(e);
const currentExceptionID = ++exceptionID;
const originalMessage = e.message || '';
let message = originalMessage;
if (e.componentStack != null) {
message += `\n\nThis 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默認(rèn)為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紅屏提示錯(cuò)誤
if (isHandledByLogBox) {
LogBoxData.addException({
...data,
isComponentError: !!e.isComponentError,
});
}
// 把調(diào)用NativeExceptionsManager上報(bào)給native
NativeExceptionsManager.reportException(data);
}
}
- NativeExceptionsManager調(diào)用native模塊上報(bào)錯(cuò)誤
// Native導(dǎo)出類,以Android為例,對(duì)應(yīng)ExceptionsManagerModule.java
const NativeModule = TurboModuleRegistry.getEnforcing<Spec>(
'ExceptionsManager',
);
const ExceptionsManager{
// 判斷是否是fatal調(diào)用不同函數(shù)上報(bào)
reportException(data: ExceptionData): void {
if (data.isFatal) {
ExceptionsManager.reportFatalException(data.message, data.stack, data.id);
} else {
ExceptionsManager.reportSoftException(data.message, data.stack, data.id);
}
},
// 上報(bào)fatal異常
reportFatalException(
message: string,
stack: Array<StackFrame>,
exceptionId: number,
) {
NativeModule.reportFatalException(message, stack, exceptionId);
},
// 上報(bào)soft異常
reportSoftException(
message: string,
stack: Array<StackFrame>,
exceptionId: number,
) {
NativeModule.reportSoftException(message, stack, exceptionId);
},
// Android提供關(guān)閉紅屏函數(shù)
dismissRedbox(): void {
if (Platform.OS !== 'ios' && NativeModule.dismissRedbox) {
// TODO(T53311281): This is a noop on iOS now. Implement it.
NativeModule.dismissRedbox();
}
},
}
console.error處理
上述提到調(diào)用ExceptionsManager.installConsoleErrorReporter處理console.error,處理成非fatal異常
function installConsoleErrorReporter() {
// 如果設(shè)置過,return
if (console._errorOriginal) {
return; // already installed
}
console._errorOriginal = console.error.bind(console);
// 設(shè)置console.error處理函數(shù)
console.error = reactConsoleErrorHandler;
if (console.reportErrorsAsExceptions === undefined) {
console.reportErrorsAsExceptions = true;
}
}
// console.error處理函數(shù),最終調(diào)用reportException上報(bào)成非fatal異常
function reactConsoleErrorHandler() {
if (arguments[0] && arguments[0].stack) {
// 上報(bào)
reportException(arguments[0], /* isFatal */ false);
} else {
// 構(gòu)造一個(gè)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';
// 上報(bào)
reportException(error, /* isFatal */ false);
}
}
注:react-native/Libraries/Core/ExceptionsManager.js
注:跟進(jìn)上述源碼可知,紅屏是通過isHandledByLogBox參數(shù)可以禁止native紅屏彈窗,isHandledByLogBox是通過global.__unstable_isLogBoxEnabled控制,可以通過下面方式禁止native紅屏展示,但是還是會(huì)展示js紅屏來提示錯(cuò)誤
global.__unstable_isLogBoxEnabled = true; YellowBox.__unstable_enableLogBox(); // 內(nèi)部調(diào)用了上面的代碼
2.2 Native部分
2.2.1 ExceptionsManagerModule
上面講述了JS處理異常后將異常拋給native處理,ExceptionsManagerModule是native處理異常模塊,導(dǎo)出給JS類名為ExceptionsManager
ExceptionsManagerModule異常處理
// 上報(bào)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);
}
// 上報(bào)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);
}
// 最終調(diào)用reportException
@ReactMethod
public void reportException(ReadableMap data) {
// 錯(cuò)誤堆棧
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()) {
// 獲取是否抑制紅屏參數(shù),對(duì)應(yīng)js側(cè)傳入的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();
}
}
// 上報(bào)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]];
}
}
// 上報(bào)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錯(cuò)誤拋出異常后為什么應(yīng)用為什么沒有退出呢?
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引擎創(chuàng)建的時(shí)候會(huì)初始化三個(gè)線程,UiThread、NativeModulesThread、JSThread,這些線程通過MessageQueueThreadHandler處理消息隊(duì)列,MessageQueueThreadHandler重寫了Handle的dispatchMessage函數(shù),函數(shù)通過try-catch包裹防止應(yīng)用直接退出,出現(xiàn)異常時(shí)調(diào)用QueueThreadExceptionHandler處理(引擎實(shí)現(xiàn)此接口),這里能攔截所有的異常,包括上述js捕獲傳到native手動(dòng)拋出的、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)的內(nèi)部類NativeExceptionHandler中,實(shí)現(xiàn)了QueueThreadExceptionHandler接口,在引擎創(chuàng)建時(shí)初始化,出現(xiàn)異常時(shí)調(diào)用NativeModuleCallExceptionHandler處理,并銷毀引擎
// 內(nèi)部類實(shí)現(xiàn)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);
}
}
}
// 調(diào)用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(引擎實(shí)現(xiàn)類)
2.2.3 最終的異常處理
默認(rèn)處理方式
上述講到引擎捕獲異常后會(huì)調(diào)用NativeModuleCallExceptionHandler.handleException處理,它是個(gè)接口,引擎提供了默認(rèn)實(shí)現(xiàn)類,默認(rèn)實(shí)現(xiàn)類收到異常后是直接拋出,會(huì)導(dǎo)致應(yīng)用退出
public interface NativeModuleCallExceptionHandler {
/** Do something to display or log the exception. */
void handleException(Exception e);
void handleCaughtException(Exception e);
}
// 默認(rèn)實(shí)現(xiàn)類
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();
}
}
自定義異常處理
為了防止默認(rèn)處理方式將異常直接拋出導(dǎo)致crash,業(yè)務(wù)可以實(shí)現(xiàn)自定義的NativeModuleCallExceptionHandler接口來處理異常,將異常上報(bào),并展示錯(cuò)誤兜底頁面
3 整體流程
基于上述源碼解析可知,RN錯(cuò)誤采集流程由JS側(cè)中MessageQueue發(fā)起,經(jīng)過一系列處理和封裝,傳到native側(cè),再經(jīng)過native一系列轉(zhuǎn)發(fā),最終交給由引擎(CatalyInstanceImple)處理,整體流程如下圖所示

4 錯(cuò)誤兜底
頁面出現(xiàn)異常后,對(duì)異常狀態(tài)兜底是一種保障線上質(zhì)量的常規(guī)手段。當(dāng)頁面發(fā)生嚴(yán)重 JS 錯(cuò)誤(FatalError)時(shí),會(huì)展示錯(cuò)誤頁面無法繼續(xù)使用。這種方式在一些業(yè)務(wù)場(chǎng)景下并不友好。比如:頁面上某一個(gè)次要模塊發(fā)生異常,并不影響核心功能的使用,這種情況下展示出錯(cuò)頁面有些不必要
React 16 中引入了一個(gè)新概念——錯(cuò)誤邊界(Error Boundaries)。錯(cuò)誤邊界是一種 React 組件,這種組件可以捕獲并打印發(fā)生在其子組件樹任何位置的 JavaScript 錯(cuò)誤,并且它會(huì)渲染出備用 UI,而不是渲染那些崩潰了的子組件樹。錯(cuò)誤邊界能在渲染期間、生命周期方法和整個(gè)組件樹的構(gòu)造函數(shù)中捕獲錯(cuò)誤
基于這個(gè)特性,業(yè)務(wù)能夠自定義控制接收到JSError的行為,能更優(yōu)雅地處理錯(cuò)誤兜底及展示
4.1 什么是錯(cuò)誤邊界
4.1.1 概念
錯(cuò)誤邊界是一種 React 組件,這種組件可以捕獲并打印發(fā)生在其子組件樹任何位置的 JS 錯(cuò)誤,并且它會(huì)渲染出備用 UI,而不是渲染那些崩潰了的子組件樹。錯(cuò)誤邊界能在渲染期間、生命周期方法和整個(gè)組件樹的構(gòu)造函數(shù)中捕獲錯(cuò)誤
4.1.2 錯(cuò)誤邊界的關(guān)鍵模塊
錯(cuò)誤邊界是通過 try-catch 方式捕獲異常的,它在哪里進(jìn)行捕獲異常的呢?React 有三個(gè)重要組成模塊,錯(cuò)誤邊界在 Reconciliation 中對(duì)異常進(jìn)行捕獲。
React基礎(chǔ)模塊(這個(gè)模塊定義了React的基礎(chǔ)API及組件相關(guān)內(nèi)容。對(duì)應(yīng)我們開發(fā)頁面時(shí)引入的 'react' 模塊)
渲染模塊(這個(gè)模塊對(duì)于不同類型的應(yīng)用,采用不同的渲染方式。對(duì)應(yīng)我們開發(fā)頁面時(shí)引入的 'react-dom' 模塊)
Reconciliation 模塊(又叫“協(xié)調(diào)模塊”,這個(gè)模塊是上面兩個(gè)模塊的基礎(chǔ),主要負(fù)責(zé)任務(wù)協(xié)調(diào)、生命周期函數(shù)管理等)
4.1.3 Reconciliation介紹
Reconciliation模塊是React三個(gè)重要模塊之一,又叫“協(xié)調(diào)模塊”,這個(gè)模塊是上面兩個(gè)模塊的基礎(chǔ),主要負(fù)責(zé)任務(wù)協(xié)調(diào)、生命周期函數(shù)管理等,它分為render和commit兩個(gè)階段
- render階段:簡單來說就是找到需要更新的工作,通過 Diff Fiber Tree 找出要做的更新工作,這是一個(gè)js計(jì)算過程,計(jì)算結(jié)果可以被緩存,計(jì)算過程可以被打斷,也可以恢復(fù)執(zhí)行。
- commit階段:提交更新并調(diào)用對(duì)應(yīng)渲染模塊(react-dom)進(jìn)行渲染,為了防止頁面抖動(dòng),該過程是同步且不能被打斷
// 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;
}
注:源碼react-native/Libraries/Renderer/ReactFabric-dev.js
錯(cuò)誤邊界不支持hooks組件,因?yàn)殄e(cuò)誤邊界的實(shí)現(xiàn)借助了this.setState可以傳遞callback的特性,useState無法傳入回調(diào),所以無法完全對(duì)標(biāo)
4.2 錯(cuò)誤邊界的使用
4.2.1 如何定義一個(gè)錯(cuò)誤邊界
前面提到錯(cuò)誤邊界捕獲異常之后會(huì)交給特定的方法處理,如果一個(gè)組件重寫了特定的方法,這個(gè)組件就是一個(gè)錯(cuò)誤邊界組件。
定義:如果一個(gè)類組件定義了生命周期方法中的任何一個(gè)(或兩個(gè))static getDerivedStateFromError() 或 componentDidCatch(),那么它就成了一個(gè)錯(cuò)誤邊界。 使用static getDerivedStateFromError()在拋出錯(cuò)誤后渲染回退UI。 使用 componentDidCatch() 來記錄錯(cuò)誤信息。如下:
export class ErrorBoundary extends Component<IProps, IState> {
constructor(props) {
super(props);
this.state = {
hasError: false
};
}
/**
* 捕獲異常,展示兜底控件。
* @param _error
*/
static getDerivedStateFromError(_error) {
return {
hasError: true
};
}
/**
*
* @param error 錯(cuò)誤信息
*/
componentDidCatch(error: Error) {
// 上報(bào)錯(cuò)誤
}
render() {
if (this.state.hasError) {
return <Text style={style.errorDesc}>出錯(cuò)了</Text>;
}
return this.props.children;
}
}
4.2.2 如何使用錯(cuò)誤邊界
將要捕獲的組件用錯(cuò)誤邊界組件包裹
export default class Example extends PureComponent<Props, State> {
render() {
return <View style={ styles.container }>
<ErrorBoundary>
{
this.renderErrorBlock()
}
</ErrorBoundary>
<Text style={ styles.other }>other block</Text>
</View>;
}
renderErrorBlock = () => {
return <View style={ styles.errorBoundary }>
'' && <Text style={ styles.error }>error block</Text>
</View>;
}
}
4.3 適用范圍
4.3.1 錯(cuò)誤邊界不能捕獲哪些異常
- 事件處理:點(diǎn)擊事件
- 異步代碼:setTimeout 或 requestAnimationFrame 回調(diào)函數(shù)等
- 錯(cuò)誤邊界自身拋出的錯(cuò)誤
4.3.2 建議使用場(chǎng)景
- 將影響整體頁面展示邏輯的模塊使用錯(cuò)誤邊界包裹并設(shè)置寬高,防止其他模塊計(jì)算出錯(cuò)
- 將非核心模塊包裹,保障在非核心模塊出錯(cuò)時(shí)核心模塊展示不受影響
- 包裹外部依賴的組件,防止意外的錯(cuò)誤
- 包裹獨(dú)立展示模塊,如廣告,活動(dòng)彈窗等
5 總結(jié)
以上就是本文全部內(nèi)容,介紹了ReactNative錯(cuò)誤采集原理及在Android中實(shí)現(xiàn)解析,后續(xù)在業(yè)務(wù)實(shí)踐過程中,我們可以根據(jù)上述分析,在JS側(cè)通過錯(cuò)誤邊界來降低特定場(chǎng)景下的錯(cuò)誤對(duì)業(yè)務(wù)的影響,并在native側(cè)做好出現(xiàn)異常時(shí)兜底,來提高頁面穩(wěn)定性和用戶體驗(yàn),同時(shí)對(duì)錯(cuò)誤統(tǒng)一收集,統(tǒng)計(jì)JS錯(cuò)誤率,改善程序穩(wěn)定性
以上就是ReactNative錯(cuò)誤采集原理在Android中實(shí)現(xiàn)詳解的詳細(xì)內(nèi)容,更多關(guān)于Android ReactNative錯(cuò)誤采集的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React實(shí)現(xiàn)過渡效果更新時(shí)間展示功能
創(chuàng)建一個(gè)組件,實(shí)時(shí)展示時(shí)分秒,并且動(dòng)態(tài)更新數(shù)據(jù),這篇文章主要介紹了React實(shí)現(xiàn)過渡效果更新時(shí)間展示功能,需要的朋友可以參考下2024-07-07
React中使用react-player 播放視頻或直播的方法
這篇文章主要介紹了React中使用react-player 播放視頻或直播,本文教大家如何使用react框架及創(chuàng)建實(shí)例的代碼,本文內(nèi)容簡短給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-01-01
React?正確使用useCallback?useMemo的方式
這篇文章主要介紹了React?正確使用useCallback?useMemo的方式,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-08-08
useReducer使用詳解及其應(yīng)用場(chǎng)景
這篇文章主要介紹了useReducer使用詳解及其應(yīng)用場(chǎng)景,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
react中代碼塊輸出,代碼高亮顯示,帶行號(hào),能復(fù)制的問題
這篇文章主要介紹了react中代碼塊輸出,代碼高亮顯示,帶行號(hào),能復(fù)制的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09

