公司最近在大力推崇使用React Native(如下簡稱RN)來開發業務組件,來代替原生業務組件,以達到快速迭代、方便熱修復等目的。雖然RN擁有比混合H5開發更好的性能體驗,性能直逼原生,可是畢竟RN是一個新的框架,可能潛在很多問題。因此,咱們但願能對RN的異常進行捕獲,並進行上報處理,以便後期分析解決這些異常,優化用戶體驗。react
RN異常在大方向上能夠分爲啓動期異常和運行期異常。下面就針對這兩種異常進行分析。bash
啓動期咱們能夠認爲從調用ReactRootView
的startReactApplication
方法開始,到ReactRootView
渲染到界面後結束。咱們先從startReactApplication
方法進行分析。框架
public void startReactApplication(
ReactInstanceManager reactInstanceManager,
String moduleName,
@Nullable Bundle initialProperties,
@Nullable String initialUITemplate) {
try {
mReactInstanceManager = reactInstanceManager;
mJSModuleName = moduleName;
mAppProperties = initialProperties;
mInitialUITemplate = initialUITemplate;
if (!mReactInstanceManager.hasStartedCreatingInitialContext()) {
mReactInstanceManager.createReactContextInBackground();
}
attachToReactInstanceManager();
} finally {
Systrace.endSection(TRACE_TAG_REACT_JAVA_BRIDGE);
}
}
複製代碼
startReactApplication
方法中調用了ReactInstanceManager
的createReactContextInBackground
方法,最終調用的是runCreateReactContextOnNewThread
方法。ide
private void runCreateReactContextOnNewThread(final ReactContextInitParams initParams) {
...省略
mCreateReactContextThread =
new Thread(
null,
new Runnable() {
@Override
public void run() {
...省略
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY);
ReactMarker.logMarker(VM_INIT);
final ReactApplicationContext reactApplicationContext =
createReactContext(
initParams.getJsExecutorFactory().create(),
initParams.getJsBundleLoader());
mCreateReactContextThread = null;
ReactMarker.logMarker(PRE_SETUP_REACT_CONTEXT_START);
final Runnable maybeRecreateReactContextRunnable =
new Runnable() {
@Override
public void run() {
if (mPendingReactContextInitParams != null) {
runCreateReactContextOnNewThread(mPendingReactContextInitParams);
mPendingReactContextInitParams = null;
}
}
};
Runnable setupReactContextRunnable =
new Runnable() {
@Override
public void run() {
try {
setupReactContext(reactApplicationContext);
} catch (Exception e) {
mDevSupportManager.handleException(e);
}
}
};
reactApplicationContext.runOnNativeModulesQueueThread(setupReactContextRunnable);
UiThreadUtil.runOnUiThread(maybeRecreateReactContextRunnable);
} catch (Exception e) {
mDevSupportManager.handleException(e);
}
}
},
"create_react_context");
mCreateReactContextThread.start();
}
複製代碼
在該方法中,咱們能夠看到當發生異常時,是有被catch住了。可是僅僅是在開發模式下才會被catch住,不然該異常就被拋出來了。函數
因此,咱們須要在這裏進行一個修改:post
在不是開發者模式時,使用自定義的ExceptionHandler去處理啓動期的異常。爲了在一個地方集中處理異常,可使用ReactInstanceManager中的ExceptionHandler進行處理。性能
運行期異常比啓動期異常要複雜一些,下面羅列了八個運行時異常的場景,若是咱們可以將這八個異常場景覆蓋住,那麼基本上就能達到目標。優化
在解析運行期異常前,咱們先來談一下RN中的運行線程。RN在初始化時維護了三個隊列,分別是:ui
所以三個隊列對應着三個線程。具體來講,JS代碼、Native代碼是運行在這幾個線程的,咱們須要理解一些關於RN的Bridge原理。但因爲這不是本文的重點,因此有興趣的同窗能夠自行查找相關文章。spa
在CatalystInstance
(其實是CatalystInstanceImpl
對象)實例化時,會初始化上面所說的三個隊列,並將它們的引用經過initializeBridge
方法傳遞給C++層的Bridge。 須要注意的是,這幾個Queue裏面都引用了MessageQueueThreadHandler
(一個Handler
對象),事件都是經過MessageQueueThreadHandler
對象post到其中的消息隊列中進行調度並執行(執行時調用dispatchMessage
方法)。而MessageQueueThreadHandler
重寫了dispatchMessage
方法,幷包裝了一層try-catch
,這使得上面所說的三個線程中發生的crash異常都能經過這裏的catch
方法進行捕獲。
@Override
public void dispatchMessage(Message msg) {
try {
super.dispatchMessage(msg);
} catch (Exception e) {
mExceptionHandler.handleException(e);
}
}
複製代碼
幸運的是,這裏的mExceptionHandler
對象就是咱們傳遞給ReactInstanceManager
的ExceptionHandler
那是否是說,咱們只要將自定義的ExceptionHandler
對象傳遞給ReactInstanceManager
,就可以統一捕獲並處理RN運行期發生的異常呢?下面咱們對上面提到的運行期的八個異常場景進行分析。
因爲調用Native模塊,是經過messageQueue.js
,而後經過C++層的Bridge,而後是NativeToJsBridge.cpp
,最後執行在了NativeQueue所在的後臺線程中。因此這個過程當中發生的異常就能夠被NativeQueue所捕獲。
道理和上面提到的同樣,Native模塊運行時是在NativeQueue所在的線程,既然這個過程當中運行異常,那麼其中的異常就會被NativeQueue所捕獲。
上面提到過,CatalystInstance
初始化時會將JSQueue
的引用傳遞到C++ Bridge,而執行JS邏輯時都會執行在該JSQueue
中。
JS調用Native模塊,是經過NativeModules.js
去查找是否有該模塊存在的。當調用一個不存在的Native模塊時,確定就發生JS錯誤了,這就會被JSQueue所捕獲。
Native去找JS模塊時,實際上是經過Java中的動態代碼,走CatalystInstanceImpl
的callFunction
方法,最後會走到NativeToJsBridge.cpp
裏面的callFunction
函數,最後仍是交給JSQueue去解析模塊、運行函數,那麼發生錯誤(JS模塊不存在,函數原型不一致)天然會被JSQueue所捕獲。
這兩個異常都是在JS運行時發生的錯誤,那麼天然會被JSQueue所捕獲。
這裏之因此將UI操做異常單獨拿出來,是由於UI操做的異常並不執行在上面所說的三個運行隊列中,因此UI操做異常就不會被上面所說的ExceptionHandler
所捕獲。 咱們知道UI操做實際上調用的是UIManagerModule
,可是這個過程並非同步的,而是有一個入隊列並調度的一個過程,以下圖所示。
用UIManager
的createView
舉例,其最後會將該UI操做的執行邏輯調度到GuardedFrameCallback
的doFrame
方法。
@Override
public final void doFrame(long frameTimeNanos) {
@Override
public final void doFrame(long frameTimeNanos) {
try {
doFrameGuarded(frameTimeNanos);
} catch (RuntimeException e) {
mReactContext.handleException(e);
}
}
複製代碼
能夠看到,這裏的異常也有被catch住了,調用的是ReactContext
的handleException
方法。
public void handleException(Exception e) {
if (mCatalystInstance != null &&
!mCatalystInstance.isDestroyed() &&
mNativeModuleCallExceptionHandler != null) {
mNativeModuleCallExceptionHandler.handleException(e);
} else {
throw new RuntimeException(e);
}
}
複製代碼
而後分發給mNativeModuleCallExceptionHandler
進行處理,而mNativeModuleCallExceptionHandler
又是經過ReactInstanceManager
進行賦值的。
private ReactApplicationContext createReactContext(
JavaScriptExecutor jsExecutor,
JSBundleLoader jsBundleLoader) {
...省略
final ReactApplicationContext reactContext = new ReactApplicationContext(mApplicationContext);
NativeModuleCallExceptionHandler exceptionHandler = mNativeModuleCallExceptionHandler != null
? mNativeModuleCallExceptionHandler
: mDevSupportManager;
reactContext.setNativeModuleCallExceptionHandler(exceptionHandler);
...省略
return reactContext;
}
複製代碼
而這裏的mNativeModuleCallExceptionHandler
就是從外面傳遞過來的,這和上面咱們傳遞給ReactInstanceManager
的ExceptionHandler
是同一個對象。
通過上面對啓動期和運行期RN異常場景的分析,咱們發現可使用自定義的一個ExceptionHandler對象對RN異常進行處理。只不過對於啓動期的異常,須要咱們對源碼進行修改,以便在非開發模式下能異常可以被咱們自定義的ExceptionHandler所捕獲。 在異常被捕獲後,須要對異常信息進一步的處理。能夠存儲到本地,也能夠發送給後臺,而後在線分析異常信息,這一步具體該如何操做就須要根據業務需求決定了。