最近在作 RN 應用線上錯誤監控的需求,在此記錄下經常使用方案。 首發地址
React Native 在架構上總體可分爲三塊:Native、JavaScript 和 Bridge。其中,Native 管理 UI 更新及交互,JavaScript 調用 Native 能力實現業務功能,Bridge 負責在兩者之間傳遞消息。java
最上層提供類 React 支持,運行在 JavaScriptCore
提供的 JavaScript 運行時環境中,Bridge 層將 JavaScript 與 Native 世界鏈接起來。react
本文從如下三個角度,分別介紹如何捕獲 RN 應用中未被處理的異常:ios
Native 有較多成熟的方案,如友盟、Bugly、網易雲捕和 crashlytics 等,這些平臺不只提供異常捕獲能力,還相應的有上報、統計、預警等能力。本文不對以上平臺異常捕獲實現方式進行分析,而是經過分析 react-native-exception-handler 瞭解 Native 端異常捕獲的實現原理。 react-native-exception-handler 實現了 setNativeExceptionHandle
用於設置 Native 監測到異常時的回調函數,以下所示:c++
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); } };
Android 提供了一個異常捕獲接口 Thread.UncaughtExceptionHandler
用於捕獲未被處理的異常。react-native-exception-handler 亦是基於此實現對 Android 端異常捕獲的,其主要代碼及分析以下所示:git
@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); } } } }); }
能夠看到,主要作了四件事:github
iOS 一般利用 NSSetUncaughtExceptionHandler
設置所有的異常處理器,當異常狀況發生時,會執行其設置的異常處理器。react-native-exception-handler 也是基於此實現對 iOS 端異常的捕獲,以下所示:web
// ==================================== // 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"); }
上述代碼主要作了三件事:objective-c
接下來,看下具體的 handleException
又作了些什麼呢?npm
// ================================================================ // 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]); }
經過對 react-native-exception-handler 源碼的解讀,能夠知道,Android 和 iOS 分別利用 Thread.UncaughtExceptionHandler
和 NSSetUncaughtExceptionHandler
實現對應用程序的異常捕獲。須要注意一點的是,當咱們重置異常處理器時,須要考慮到其已存在的異常處理邏輯,避免將其直接覆蓋,致使其餘監測處理程序失效。segmentfault
爲了解決部分 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 回調函數)
- 服務端渲染
- 它自身拋出來的錯誤(並不是它的子組件)
上文中提到,Error Boundaries 能捕獲子組件生命週期函數中的異常,包括構造函數(constructor)和 render 函數。而沒法捕獲如下異常:
對於這些錯誤邊界沒法捕獲的異常,在 web 中能夠經過 window.onerror() 加載一個全局的error
事件處理函數用於自動收集錯誤報告。
那麼 React Native 中是如何處理的呢?
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 中獲取到。
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 有三個做用:
查看 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 源碼中,能夠看到,其定義了多個變量以及四個函數:
而以上四個函數的調用時機則是交給 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 來決定是否對異常進行捕獲。
/** * 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); });
那麼 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
除了上述提到的幾種致使 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⭐️。