🔥React Native 全方位異常監控

最近在作 RN 應用線上錯誤監控的需求,在此記錄下經常使用方案。 首發地址

0. 開始

React Native 在架構上總體可分爲三塊:Native、JavaScript 和 Bridge。其中,Native 管理 UI 更新及交互,JavaScript 調用 Native 能力實現業務功能,Bridge 負責在兩者之間傳遞消息。java

rn-architecture.png

最上層提供類 React 支持,運行在 JavaScriptCore 提供的 JavaScript 運行時環境中,Bridge 層將 JavaScript 與 Native 世界鏈接起來react

本文從如下三個角度,分別介紹如何捕獲 RN 應用中未被處理的異常:ios

  • Native 異常捕獲;
  • JS 異常捕獲;
  • React 異常捕獲;

1. Native 異常捕獲

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);
  }
};

1.1 Android 異常捕獲

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

  • 實例化 new Thread.UncaughtExceptionHandler(),並重寫其 uncaughtException 方法;
  • uncaughtException 方法中執行 JS 回調函數;
  • 兼容自定義 Native Exception handler 的狀況;
  • 調用 Thread.setDefaultUncaughtExceptionHandler 重置異常處理器;

1.2 iOS 異常捕獲

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

  • 設置異常處理函數用於執行 JS 回調;
  • 獲取已存在的 native 異常處理器;
  • 利用 NSSetUncaughtExceptionHandler 自定義異常處理器 HandleException;

接下來,看下具體的 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]);
    
}

1.3 小結

經過對 react-native-exception-handler 源碼的解讀,能夠知道,Android 和 iOS 分別利用 Thread.UncaughtExceptionHandlerNSSetUncaughtExceptionHandler 實現對應用程序的異常捕獲。須要注意一點的是,當咱們重置異常處理器時,須要考慮到其已存在的異常處理邏輯,避免將其直接覆蓋,致使其餘監測處理程序失效。segmentfault

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

rn-messagequeue.png

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_log.png

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>
  );
};

當點擊屏幕按鈕時,可在控制檯上看到以下信息:

MessageQueue_demo.png

能夠看到,當 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 異常時,控制檯輸出黃色警告⚠️:

rn_yellow_log.png

而設備則表現爲彈出黃屏:

<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⭐️

推薦閱讀

參考文章

相關文章
相關標籤/搜索