接下来,我们来聊聊 RN 是如何处理运行时异常的
首先,我们需要先考虑下异常的来源
由于 RN 是一个跨语言跨端的框架,所以我们可以根据异常的来源划分为:
- JS 侧的异常:
ReferenceError,TypeError等等 - 端侧的异常:IOS 的
NSException,Android 的RuntimeException等等
其次,我们需要考虑异常该由谁来处理:
- 在浏览器中,未捕获的异常最后会被 JS 运行时(比如 V8)兜住,最后交给宿主平台(浏览器)处理
- 在 RN 中,逻辑类似,也需要有一套逻辑把未捕获异常交给原生平台(IOS,Android)处理
为了做到这件事,我们需要:
- 在 JS 侧收集未捕获异常,并发送给 Native
- 在 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 内部主要做了两件事:
- 把异常分成了两种:致命与非致命(isFatal),并且将其发送给了不同的 Native 处理方法
- 给原来的
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 暴露了两个方法:reportFatalException 与 reportSoftException
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 &¶ms, 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 ¶ms) {
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
重要的是,贴合业务具体场景,选择最适合的处理方案