RN 的初版架构——运行时异常与异常捕获处理

接下来,我们来聊聊 RN 是如何处理运行时异常的

首先,我们需要先考虑下异常的来源

由于 RN 是一个跨语言跨端的框架,所以我们可以根据异常的来源划分为:

  • JS 侧的异常:ReferenceErrorTypeError 等等
  • 端侧的异常:IOS 的 NSException,Android 的 RuntimeException 等等

其次,我们需要考虑异常该由谁来处理:

  • 在浏览器中,未捕获的异常最后会被 JS 运行时(比如 V8)兜住,最后交给宿主平台(浏览器)处理
  • 在 RN 中,逻辑类似,也需要有一套逻辑把未捕获异常交给原生平台(IOS,Android)处理

为了做到这件事,我们需要:

  1. 在 JS 侧收集未捕获异常,并发送给 Native
  2. 在 Native 中按照平台特性处理这些异常

接下来,我们将深入 RN 内部,一起讨论 RN 是如何做到这两件事的

JS 侧

同步异常

JS 侧在异常处理中最重要的职责就是尽可能的收集所有未捕获异常,然后发送给 Native

我们都知道实际上 RN 就是一个 Native-Bridge-JS 的三层架构,而 JS 中所有代码的触发都来源于 Native 的调用

在 JS 中,所有来自 Native 的调用都会集中在 MessageQueue.js 中,这也是我们收集未捕获异常的好时机

事实上,RN 也是这么做的,我们来看看 MessageQueue.js 中发生了什么:

js 复制代码
 // in MessageQueue.js
 class MessageQueue {
   // ...省略若干代码
   
   // Native 调用 JS 模块方法
   callFunctionReturnFlushedQueue(module: string, method: string, args: any[]) {
     this.__guard(() => {
       this.__callFunction(module, method, args);
     });
 ​
     return this.flushedQueue();
   }
 ​
   // Native 调用 JS 模块方法并返回结果
   callFunctionReturnResultAndFlushedQueue(
     module: string,
     method: string,
     args: any[],
   ) {
     let result;
     this.__guard(() => {
       result = this.__callFunction(module, method, args);
     });
 ​
     return [result, this.flushedQueue()];
   }
 ​
   // Native 调用 JS 模块回调
   invokeCallbackAndReturnFlushedQueue(cbID: number, args: any[]) {
     this.__guard(() => {
       this.__invokeCallback(cbID, args);
     });
 ​
     return this.flushedQueue();
   }
 ​
   /**
    * Private methods
    */
   __guard(fn: () => void) {
     // 用于内部调试,可以暂时忽略
     if (this.__shouldPauseOnThrow()) {
       fn();
     } else {
       // 重点看这里
       try {
         fn();
       } catch (error) {
         ErrorUtils.reportFatalError(error);
       }
     }
   }
 ​
   // ...省略若干代码
 }

可以看到这三个 Native 调用 JS 模块的方法都使用了 this.__guard 来包裹

__guard 中做了一件事:捕获调用过程中的未捕获异常,并且将其交给 ErrorUtils 模块的 reportFatalError 方法处理

我们来看看 ErrorUtils 都做了些什么:

js 复制代码
 // error-guard.js
 const ErrorUtils = {
   setGlobalHandler(fun) {
     _globalHandler = fun;
   },
   getGlobalHandler() {
     return _globalHandler;
   },
   // 上报非致命异常
   reportError(error) {
     _globalHandler && _globalHandler(error);
   },
   // 上报致命异常
   reportFatalError(error) {
     _globalHandler && _globalHandler(error, true);
   },
   
   // ...省略部份代码
 };

可以看到,ErrorUtils 只是帮忙持有了 _globalHandler,并且在中间做了一层转发而已

而真正的 handler 在项目初始化的时候就已经被注入了:

js 复制代码
 // in InitializeCore.js
 ​
 // ...省略部份代码
 // Set up console
 const ExceptionsManager = require('ExceptionsManager');
 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('ErrorUtils');
   ErrorUtils.setGlobalHandler(handleError);
 }
 ​
 // ...省略部份代码

我们终于找到了源头:ExceptionsManager 就是负责处理异常的核心模块

ExceptionsManager 内部主要做了两件事:

  1. 把异常分成了两种:致命与非致命(isFatal),并且将其发送给了不同的 Native 处理方法
  2. 给原来的 console.error 加了个装饰器,以非致命的形式将 error 的内容发给了 Native
js 复制代码
 // in ExceptionsManager.js
 ​
 // ...省略部份代码
 function reportException(e: ExtendedError, isFatal: boolean) {
   const {ExceptionsManager} = require('NativeModules');
   if (ExceptionsManager) {
     const parseErrorStack = require('parseErrorStack');
     const stack = parseErrorStack(e);
     const currentExceptionID = ++exceptionID;
     // 根据异常的严重程度调用不同的 Native 处理方法
     if (isFatal) {
       ExceptionsManager.reportFatalException(
         e.message,
         stack,
         currentExceptionID,
       );
     } else {
       ExceptionsManager.reportSoftException(
         e.message,
         stack,
         currentExceptionID,
       );
     }
     // ...省略部份代码
   }
 }
 ​
 // console.error 的装饰器
 function reactConsoleErrorHandler() {
   console._errorOriginal.apply(console, arguments);
   if (!console.reportErrorsAsExceptions) {
     return;
   }
 ​
   if (arguments[0] && arguments[0].stack) {
     reportException(arguments[0], /* isFatal */ false);
   } else {
     const stringifySafe = require('stringifySafe');
     const str = Array.prototype.map.call(arguments, stringifySafe).join(', ');
 ​
     // ... 省略部份代码
 ​
     const error: ExtendedError = new Error('console.error: ' + str);
     error.framesToPop = 1;
     // 发送非致命异常给 Native
     reportException(error, /* isFatal */ false);
   }
 }
 ​
 // 在上述 InitializeCore.js 中被调用的方法,职责是给 console.error 加装饰器
 function installConsoleErrorReporter() {
   // Enable reportErrorsAsExceptions
   if (console._errorOriginal) {
     return; // already installed
   }
   // Flow doesn't like it when you set arbitrary values on a global object
   console._errorOriginal = console.error.bind(console);
   console.error = reactConsoleErrorHandler;
   if (console.reportErrorsAsExceptions === undefined) {
     // Individual apps can disable this
     // Flow doesn't like it when you set arbitrary values on a global object
     console.reportErrorsAsExceptions = true;
   }
 }

之所以要区分致命/非致命异常,是因为 RN 希望开发者能在 DEV 模式下获得更多信息,这两种异常在 PROD/DEV 模式下的表现可以概括如下:

致命异常 非致命异常
DEV RedBox(红屏) + 应用终止 RedBox / LogBox
PROD 应用闪退 打印日志 / 忽略异常

致命异常的典型特征是从被调用的 JS 方法的边界逃逸出去,比如:

  • 组件内部抛出异常但是没有设立 ErrorBoundary
  • AppRegistry.runApplication 内部抛出的未捕获异常

此时我们应该做的就是:立刻终止当前应用

非致命异常指的是那些没有从 JS 方法边界逃逸的软性异常,最常见的就是 console.error,这类异常仅作为警告作用,通常不会影响程序运行

异步异常

在 JS 中还有一类异常,它们不会直接的影响程序可用性,但是它们一旦出现,一般标识着不太好的信号

这类异常有时甚至比直接崩溃的危害更大,因为它往往是静默的,甚至可能会导致一些偶现问题

这就是异步异常

为了侦测这类异常,浏览器提供了 unhandledrejection 事件或 onunhandledrejection 属性来让开发者监控此类异常,其用法如下:

js 复制代码
 window.addEventListener("unhandledrejection", (event) => {
   console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
 });
 ​
 window.onunhandledrejection = (event) => {
   console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`);
 };

而在 RN 中,是没有这些事件与属性的,所以 RN 采取了曲线救国的方法:使用一个 polyfill 的 Promise(相关 Repo

RN 在 InitializeCore.js 中使用了新的 Promise 覆盖了原来的:

js 复制代码
 // in InitializeCore.js
 ​
 // Set up Promise
 // The native Promise implementation throws the following error:
 // ERROR: Event loop not supported.
 polyfillGlobal('Promise', () => require('Promise'));

这个 Promise 来自 Libraries/Promise.js

js 复制代码
 // in Promise.js
 ​
 // 这个文件 fork 了 https://github.com/then/promise/tree/master
 const Promise = require('fbjs/lib/Promise.native');
 ​
 if (__DEV__) {
   // 这里使用了上述 promise 仓库中的其中一个文件,本质上是在提供的原始 Promise 后面挂了一个针对 reject 的监听
   // 感兴趣的可以看看 https://github.com/then/promise/blob/master/src/rejection-tracking.js#L20
   require('promise/setimmediate/rejection-tracking').enable({
     allRejections: true,
     onUnhandled: (id, error = {}) => {
       let message: string;
       let stack: ?string;
 ​
       const stringValue = Object.prototype.toString.call(error);
       if (stringValue === '[object Error]') {
         message = Error.prototype.toString.call(error);
         stack = error.stack;
       } else {
         message = require('pretty-format')(error);
       }
 ​
       const warning =
         `Possible Unhandled Promise Rejection (id: ${id}):\n` +
         `${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);
     },
   });
 }

可以看到,在 RN polifill 的 Promise 中,针对未被捕获的 reject 做了一个 console.warn 的操作

采取了一个 "提醒但不干扰" 的解决方案

至此,JS 侧的异常处理职责结束,该轮到 Native 接锅了

Native 侧

Android

在 Android 中,针对异常处理主要有两个职责:

  • 处理 JS 传过来的致命/非致命异常
  • 处理 Native module 执行过程的异常

首先来看对 JS 侧传来异常的处理

JS 的异常是由 ExceptionsManagerModule 负责处理,它对 JS 暴露了两个方法:reportFatalExceptionreportSoftException

java 复制代码
 // in ExceptionsManagerModule.java
 ​
 @ReactModule(name = ExceptionsManagerModule.NAME)
 public class ExceptionsManagerModule extends BaseJavaModule {
   protected static final String NAME = "ExceptionsManager";
 ​
   @ReactMethod
   public void reportFatalException(String title, ReadableArray details, int exceptionId) {
     // 处理致命异常
     showOrThrowError(title, details, exceptionId);
   }
 ​
   @ReactMethod
   public void reportSoftException(String title, ReadableArray details, int exceptionId) {
     // 处理非致命异常
     if (mDevSupportManager.getDevSupportEnabled()) {
       mDevSupportManager.showNewJSError(title, details, exceptionId);
     } else {
       FLog.e(ReactConstants.TAG, JSStackTrace.format(title, details));
     }
   }
 ​
   private void showOrThrowError(String title, ReadableArray details, int exceptionId) {
     if (mDevSupportManager.getDevSupportEnabled()) {
       mDevSupportManager.showNewJSError(title, details, exceptionId);
     } else {
       // 抛出原生 JavascriptException
       throw new JavascriptException(JSStackTrace.format(title, details));
     }
   }
 }

可以看到,在 ExceptionsManagerModule 内部的方法还针对不同的场景(DEV/PROD)区分了不同的应对情况

其中 mDevSupportManager.showNewJSError 本质上就是调出 RedBox

如果当前在 PROD 场景且发生了致命异常,ExceptionsManagerModule 则会直接向外抛出一个 JavascriptException(第 27 行)

这个异常在没有额外的干预下,会导致应用闪退

接着我们来看看 Native module 如果发生异常该怎么处理

我们先放一个流程图,后面会详细说明:

arduino 复制代码
  JS 调用 Native module
→ RN 找到 Native module 对应的线程,并且发送调用信息(通过 dispatchMessage 发送)
→ RN 用 MessageQueueThreadHandler 重载了 dispatchMessage 方法,植入了一个 try-catch
→ Native module 出现异常,被 catch 捕获后被 NativeExceptionHandler 的 handleException 消费
→ handleException 做了两件事:
	1. 调用 NativeModuleCallExceptionHandler 的 handleException 方法
	2. 调用 UI 线程中的 destroy 方法拆掉 RN 运行时
→ 如果 handleException 中重新抛出错误,整个 APP 会闪退;否则,整个 APP 会只剩下空壳(因为 RN 运行时被拆了)

在本专栏的前文有说过,RN 的不同 Native module 在 Android 中取决于具体配置有可能存在于不同的线程中

所以从 JS 到 Native module 的调用势必需要经过跨线程的流程,而 RN 通过重载其中的关键方法,把所有 Native module 出现的异常全部兜住了,具体代码如下:

java 复制代码
// in MessageQueueThreadHandler.java

/**
 * Handler that can catch and dispatch Exceptions to an Exception handler.
 */
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) {
      // 关键,把所有 Native module 的逻辑都用 try-catch 包住了并委托给 mExceptionHandler
      mExceptionHandler.handleException(e);
    }
  }
}

其中 handleException 的实现被放在了 CatalystInstanceImpl.java 中(这个文件主要管 Android 侧的 bridge 运行时)

java 复制代码
// in CatalystInstanceImpl.java

public class CatalystInstanceImpl implements CatalystInstance {
  // ...省略部份代码
  private void onNativeException(Exception e) {
    // 调用 NativeModuleCallExceptionHandler 的异常处理方法
    mNativeModuleCallExceptionHandler.handleException(e);
    // 调用 UI 线程的 destroy 方法,把 RN 运行时拆掉
    mReactQueueConfiguration.getUIQueueThread().runOnQueue(
      new Runnable() {
        @Override
        public void run() {
          destroy();
        }
      });
  }

  private class NativeExceptionHandler implements QueueThreadExceptionHandler {
    @Override
    public void handleException(Exception e) {
      onNativeException(e);
    }
  }
	// ...省略部份代码
}

NativeModuleCallExceptionHandler 非常有意思,因为它是 RN APP 开发者在 RN 框架范畴内唯一能感知到 Native module 异常的机制,要认识如何使用 NativeModuleCallExceptionHandler,我们需要先了解一下 ReactInstanceManager

ReactInstanceManager 主要负责 Android 中关于 RN 的所有运行时管理,包括 JS 引擎、Bridge、JS bundle 加载、错误处理等等

上面我们说把运行时拆掉的 destroy 方法的实现也在这个文件中

ReactInstanceManager 需要通过 ReactInstanceManagerBuilder 构建,其中有一个公共方法 setNativeModuleCallExceptionHandler,这个方法负责设置一个处理所有 Native module 抛出来异常的方法

RN APP 的开发者可以通过在自己项目的 MainApplication.java 中重载 createReactInstanceManager 方法来定制一个自己的 NativeModuleCallExceptionHandler

java 复制代码
// in MainApplication.java(Your repo)

public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    @Override
    protected ReactInstanceManager createReactInstanceManager() {
      ReactInstanceManagerBuilder builder =
          ReactInstanceManager.builder()
              .setApplication(getApplication())
              .setCurrentActivity(null)
              .setBundleAssetName("index.android.bundle")
              .setJSMainModulePath("index")
              .addPackages(getPackages())
              .setUseDeveloperSupport(getUseDeveloperSupport())
              .setInitialLifecycleState(LifecycleState.BEFORE_CREATE);

      builder.setNativeModuleCallExceptionHandler(e -> {
        // 可以在这里打印日志、展示异常 ui、重启应用等等
        // 注意如果在这里吞掉了异常,Android 依然会把 RN 相关的运行时拆掉,整个 APP 会只剩下一个空壳
        // 如果这里正常把异常再度抛出,在没有特别设置的情况下会使应用闪退(在生产模式下,这个结果有时是符合预期的)
      });

      return builder.build();
    }

    // ...省略部份代码
  };

  // ...省略部份代码
}

既然我们可以覆盖,那就表示有默认的实现,接下来我们来看看 RN 默认的 NativeModuleCallExceptionHandler 做了哪些操作

java 复制代码
// in DevSupportManagerImpl.java

// ...省略部份代码
@Override
public void handleException(Exception e) {
  // 如果开发模式打开了
  if (mIsDevSupportEnabled) {
    // 只打印日志,不抛出异常,此时 APP 不会闪退,但是会只剩一个空壳子
    for (ExceptionLogger logger : mExceptionLoggers) {
      logger.log(e);
    }
  } else {
    // 这部分代码在下面的 DefaultNativeModuleCallExceptionHandler.java 中
    mDefaultNativeModuleCallExceptionHandler.handleException(e);
  }
}
// ...省略部份代码

// in DefaultNativeModuleCallExceptionHandler.java

// ...省略部份代码
public class DefaultNativeModuleCallExceptionHandler implements NativeModuleCallExceptionHandler {
  @Override
  public void handleException(Exception e) {
    // 直接把异常原封不动抛出去了,此时应用会直接闪退
    if (e instanceof RuntimeException) {
      throw (RuntimeException) e;
    } else {
      throw new RuntimeException(e);
    }
  }
}

至此,我们聊完了 Android 的部份,接下来我们聊聊 IOS 的部份

IOS

与 Android 类似,IOS 针对异常处理也有对称的两个职责:

  • 处理 JS 传过来的致命/非致命异常
  • 处理 Native module 执行过程的异常

首先来看对 JS 侧传来异常的处理:与 Android 类似,IOS 也有一个针对 JS 异常的 Native module

objc 复制代码
// in RCTExceptionsManager.m

// 处理非致命异常
RCT_EXPORT_METHOD(reportSoftException:(NSString *)message
                  stack:(NSArray<NSDictionary *> *)stack
                  exceptionId:(nonnull NSNumber *)exceptionId)
{
  // 展示 RedBox
  [_bridge.redBox showErrorMessage:message withStack:stack];

  // 如果有 delegate,调用其中的 handleSoftJSExceptionWithMessage 方法
  // 这个逻辑是为了让 APP 开发者可以有机会感知到 JS 未处理的非致命异常而搭建的
  // 后文会用一个例子来讲解如何使用这个机制
  if (_delegate) {
    [_delegate handleSoftJSExceptionWithMessage:message stack:stack exceptionId:exceptionId];
  }
}

// 处理致命异常
RCT_EXPORT_METHOD(reportFatalException:(NSString *)message
                  stack:(NSArray<NSDictionary *> *)stack
                  exceptionId:(nonnull NSNumber *)exceptionId)
{
  // 与非致命异常一致,展示 RedBox
  [_bridge.redBox showErrorMessage:message withStack:stack];

  // 如果有 delegate,调用其中的 handleFatalJSExceptionWithMessage 方法
  // 与非致命异常一致,这个逻辑是为了让 APP 开发者可以有机会感知到 JS 未处理的非致命异常而搭建的
  if (_delegate) {
    [_delegate handleFatalJSExceptionWithMessage:message stack:stack exceptionId:exceptionId];
  }

  static NSUInteger reloadRetries = 0;
  // _maxReloadAttempts 是一个可以让 APP 开发者自己设置的值,默认情况下是 0(也就是发生致命异常马上抛异常)
  // 如果设置为 1,则会允许 RN reload 一次 bridge,如果 reload 后再发生致命异常,则会直接抛出
  // 后文会用一个例子来介绍如何设置这个值,但这是一个"除非你知道后果,不然不要轻易使用"的机制
  if (!RCT_DEBUG && reloadRetries < _maxReloadAttempts) {
    reloadRetries++;
    [_bridge reload];
  } else {
    // 直接抛出 IOS 层面的异常
    NSString *description = [@"Unhandled JS Exception: " stringByAppendingString:message];
    NSDictionary *errorInfo = @{ NSLocalizedDescriptionKey: description, RCTJSStackTraceKey: stack };
    RCTFatal([NSError errorWithDomain:RCTErrorDomain code:0 userInfo:errorInfo]);
  }
}

// 设置 _delegate 的方法,后文会介绍如何使用
- (instancetype)initWithDelegate:(id<RCTExceptionsManagerDelegate>)delegate
{
  if ((self = [self init])) {
    _delegate = delegate;
  }
  return self;
}

可以看到 IOS RCTExceptionsManager 跟 Android 的逻辑基本一致,区别在于它多了一个 delegate

接下来我们来聊一下如何使用这个 delegate

要设置 delegate,我们需要修改我们的 RN 项目中的 AppDelegate.m

objc 复制代码
// in AppDelegate.m(your repo)

@implementation AppDelegate

- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
  return [[RCTBundleURLProvider sharedSettings]
          jsBundleURLForBundleRoot:@"index"
          fallbackResource:nil];
}

/**
 * ⚠️关键部分
 * extraModulesForBridge 会被 RCTCxxBridge.mm 中的 registerExtraModules 方法调用
 * 这个方法会覆盖原来 RCTExceptionsManager 的实现,由于我们的实例带着 delegate,所以发生异常的时候会被调用
 */
- (NSArray<id<RCTBridgeModule>> *)extraModulesForBridge:(RCTBridge *)bridge
{
  MyExceptionsDelegate *delegate = [MyExceptionsDelegate new];
  // 初始化自己的 RCTExceptionsManager,并且带上我们自己的 delegate
  RCTExceptionsManager *manager =
    [[RCTExceptionsManager alloc] initWithDelegate:delegate];
  // 设置出现致命异常是允许 reload bridge 的次数,默认为 0
  manager.maxReloadAttempts = 1;

  return @[ manager ];
}

- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self
                                           launchOptions:launchOptions];

  RCTRootView *rootView =
    [[RCTRootView alloc] initWithBridge:bridge
                             moduleName:@"Demo"
                      initialProperties:nil];

  rootView.backgroundColor = [UIColor whiteColor];

  self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
  UIViewController *vc = [UIViewController new];
  vc.view = rootView;
  self.window.rootViewController = vc;
  [self.window makeKeyAndVisible];

  return YES;
}

@end

AppDelegate.m 的框架搭好了,接下来是 MyExceptionsDelegate 的实现:

objc 复制代码
// in MyExceptionsDelegate.m(your repo)

@implementation MyExceptionsDelegate

- (void)handleFatalJSExceptionWithMessage:(NSString *)message
                                   stack:(NSArray<NSDictionary *> *)stack
                             exceptionId:(NSNumber *)exceptionId
{
  // 可以打印日志,可以做分析,也可以主动抛错
  NSLog(@"[MyDelegate] FATAL JS ERROR: %@", message);
}

- (void)handleSoftJSExceptionWithMessage:(NSString *)message
                                  stack:(NSArray<NSDictionary *> *)stack
                            exceptionId:(NSNumber *)exceptionId
{
	// 可以打印日志,可以做分析,也可以主动抛错
  NSLog(@"[MyDelegate] SOFT JS ERROR: %@", message);
}

@end

至此,我们了解了 IOS 是如何处理 JS 传递过来的异常,以及我们如何在过程中用 delegate 的方式记录这个异常

接下来,我们来看看如果 Native module 发生了异常该怎么处理

跟 Android 一样,我们先放一个流程图,然后用代码来解释 IOS 是如何处理的:

arduino 复制代码
  JS 调用 Native module 的方法
→ RN 找到对应的模块,通过 invoke 调用
→ 在 invoke 方法中找到对应的 Queue(代表 IOS 的线程),调用 dispatch_async 方法在对应的 Queue 执行对应方法
→ 在 invoke 的内部方法 invokeInner 中用 try-catch 捕获 Native module 执行过程中的异常
→ 在 catch 块中把捕获的异常交给 RCTFatal 处理
→ 在 RCTFatal 中,开发者可以通过注册 RCTFatalHandler 获得处理异常的机会,否则这个异常会被当成 NSException 被抛出,最后造成应用闪退

在本专栏的前文有说过,RN 的不同 Native module 在 IOS 中取决于具体配置有可能存在于不同的 Queue 中(类似 Android 的线程)

所以从 JS 到 Native module 的调用势必需要经过跨线程的流程,其中的第一步就是找到对应的模块,然后通过 invoke 方法调用:

cpp 复制代码
// in ModuleRegistry.cpp

void ModuleRegistry::callNativeMethod(unsigned int moduleId, unsigned int methodId, folly::dynamic&& params, int callId) {
  if (moduleId >= modules_.size()) {
    throw std::runtime_error(
      folly::to<std::string>("moduleId ", moduleId, " out of range [0..", modules_.size(), ")"));
  }
  // 找到对应模块,调用模块的 invoke 方法
  modules_[moduleId]->invoke(methodId, std::move(params), callId);
}

调用后会进入该模块的 invoke 方法,以下是对应实现:

objc 复制代码
// RCTNativeModule.mm

void RCTNativeModule::invoke(unsigned int methodId, folly::dynamic &&params, int callId) {
  __weak RCTBridge *weakBridge = m_bridge;
  __weak RCTModuleData *weakModuleData = m_moduleData;
	// 构建需要在对应 Queue 中执行的代码块
  dispatch_block_t block = [weakBridge, weakModuleData, methodId, params=std::move(params), callId] {
    #ifdef WITH_FBSYSTRACE
    if (callId != -1) {
      fbsystrace_end_async_flow(TRACE_TAG_REACT_APPS, "native", callId);
    }
    #endif
    // 这个方法很重要,是具体执行 native module 方法的方法
    invokeInner(weakBridge, weakModuleData, methodId, std::move(params));
  };

  if (m_bridge.valid) {
    // 从该 module 找到对应的 Queue
    dispatch_queue_t queue = m_moduleData.methodQueue;
    if (queue == RCTJSThread) {
      // 如果对应的 Queue 就是当前的 Queue,直接执行
      block();
    } else if (queue) {
      // 否则使用 dispatch_async 跨线程调用
      dispatch_async(queue, block);
    }
  } else {
    RCTLogWarn(@"Attempted to invoke `%u` (method ID) on `%@` (NativeModule name) with an invalid bridge.", methodId, m_moduleData.name);
  }
}

可以看到,其中最关键的就是在对应的 Queue 中调用了 invokeInner 方法:

objc 复制代码
// RCTNativeModule.mm

static MethodCallResult invokeInner(RCTBridge *bridge, RCTModuleData *moduleData, unsigned int methodId, const folly::dynamic &params) {
  if (!bridge || !bridge.valid || !moduleData) {
    return folly::none;
  }

  // 找到需要调用的方法
  id<RCTBridgeMethod> method = moduleData.methods[methodId];
  if (RCT_DEBUG && !method) {
    RCTLogError(@"Unknown methodID: %ud for module: %@",methodId, moduleData.name);
  }

  // 构建参数
  NSArray *objcParams = convertFollyDynamicToId(params);
  @try {
    // 调用 Native module 的方法
    id result = [method invokeWithBridge:bridge module:moduleData.instance arguments:objcParams];
    return convertIdToFollyDynamic(result);
  }
  @catch (NSException *exception) {
		// 如果是 RN 内部异常,直接抛出
    if ([exception.name hasPrefix:RCTFatalExceptionName]) {
      @throw exception;
    }

    NSString *message = [NSString stringWithFormat:@"Exception '%@' was thrown while invoking %s on target %@ with params %@\ncallstack: %@",exception, method.JSMethodName, moduleData.name, objcParams, exception.callStackSymbols];
    // 否则交给 RCTFatal 处理
    RCTFatal(RCTErrorWithMessage(message));
  }
  return folly::none;
}

接下来我们看看 RCTFatal 内部干了什么:

objc 复制代码
// in RCTAssert.m

void RCTFatal(NSError *error)
{
  _RCTLogNativeInternal(RCTLogLevelFatal, NULL, 0, @"%@", error.localizedDescription);

  // 获取开发者设置的 RCTFatalHandler
  RCTFatalHandler fatalHandler = RCTGetFatalHandler();
  if (fatalHandler) {
    // 如果有就执行 RCTFatalHandler
    fatalHandler(error);
  } else {
#if DEBUG
    @try {
#endif
      NSString *name = [NSString stringWithFormat:@"%@: %@", RCTFatalExceptionName, error.localizedDescription];
      NSString *message = RCTFormatError(error.localizedDescription, error.userInfo[RCTJSStackTraceKey], 75);
      // 如果没有 RCTFatalHandler 就直接抛出异常
      @throw [[NSException alloc]  initWithName:name reason:message userInfo:nil];
#if DEBUG
    } @catch (NSException *e) {}
#endif
  }
}

可以看到,如果我们设置了 RCTFatalHandler,我们就可以覆盖默认的逻辑,执行我们指定的异常处理逻辑

那么应该怎么设置 RCTFatalHandler 呢?

objc 复制代码
// in AppDelegate.m(Your repo)

#import <React/RCTAssert.h>

static void MyFatalHandler(NSError *error) {
  // 打印日志、上报异常等等
}

@implementation AppDelegate
- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  // 在这里设置 MyFatalHandler
  RCTSetFatalHandler(MyFatalHandler);

  // ...省略部份代码
  return YES;
}
@end

简单一行代码搞定👍

总结

在上面的章节中,我们聊到了 RN 捕获 JS 异常的模块 ErrorUtils 以及原生平台是如何通过 ExceptionsManager 这个模块处理异常的

此外,我们还聊到了原生平台是如何捕获并处理它们自己的 Native Module 异常

在日常开发中,我们除了了解并利用这些 RN 提供的异常处理方案之外,还可以结合原生平台本身的异常处理机制来使用,此外市面上也有很多专注于 APP 的异常处理的解决方案,比如 Sentry

重要的是,贴合业务具体场景,选择最适合的处理方案

相关推荐
web小白成长日记19 小时前
前端三个月速成,是否靠谱?
前端·react.js·前端框架·html·reactjs·webkit·scss
卧指世阁1 天前
不从零开始构建专属 SVG 编辑器的实战指南
前端·javascript·前端框架
Eadia1 天前
React基础框架搭建7-测试:react+router+redux+axios+Tailwind+webpack
react.js·架构·前端框架
HarrySunCn1 天前
vite.config.js 代理配置
前端·前端框架
大猫会长1 天前
react组件外的变量是共用的
前端·react.js·前端框架
开心不就得了1 天前
React Native对接Sunmi打印sdk
javascript·react native·react.js
hxjhnct1 天前
React 父组件调用子组件的方法
前端·react.js·前端框架
技术宅小温1 天前
< uni-app开发核心难点解析:框架适配与打包发布全流程踩坑指南 >
前端·前端框架·uni-app·web app
web小白成长日记2 天前
前端让我明显感受到了信息闭塞的恐怖......
前端·javascript·css·react.js·前端框架·html