原生專案如何從零開始整合 React Native

xiangzhihong發表於2022-05-24

一、混合開發

App 混合開發,指的是一個 App 部分功能用 Native 構建,其他功能使用跨端框架進行構建,最常見的場景是,Native 作為一個可工程,其實業務開發使用垮端框架進行開發。目前,比較流行的跨端框架有 H5、React Native、Flutter、佈局動態化等。
在這裡插入圖片描述

而在以 Native 與 React Native 混合開發中,同一個 App 中,混合開發通常有以下幾種形態:
在這裡插入圖片描述

那究竟有哪些公司在使用 React Native 呢?

首先,是一些大型 App 中,比如美團、攜程、京東、58 等。這些大型 App 一般都非常複雜,整個框架需要包含模組化、元件化、外掛化、跨端等能力。相比純 React Native 工程,大型 App 的實際業務開發還需要考慮如何在已有原生專案中引入 React Native,作為原生框架的擴充套件能力。比如,部分業務可能對開發效率、多端一致性、動態更新能力有較強要求,就可以使用 React Native 來實現。

除了大型 App 外,如果想要在已上線的專案引入 React Native,也需要使用混合開發模式。因為原生改造成 React Native 並不是那麼簡單的事情,畢竟開發語言、頁面管理、模組管理完全是另一套東西,而且一些原生頁面,如啟動頁、首頁等,出於效能考慮,大都還是會選擇原生來實現,對一些業務要求不高的業務則使用 React Native 進行開發。

當然,對於一些新開發的輕量級 App,完全可以選擇純 React Native 模式。因為新開發的 App 沒有技術債,可以從 0 到 1 享受 React Native 跨端的優勢。那使用 React Native 模式有哪些優勢呢?

  • 開發效率高,一套程式碼可以在 Android、iOS 上執行;
  • 更新部署方便,無須依賴應用市場發版,迭代更新速度快;
  • 具備動態更新能力,特別是國內 App,以 Android 為例,受限於 Google Play,無法使用 Android App bundle,而外掛化框架又存在穩定性問題,而 React Native 可以解決這一痛點。

當然,也有一些缺點:

  • 效能不佳:H5 渲染鏈路長;React Native 依託於 JS bridge 互動 (舊版,最新架構使用 JSI);雖然 Flutter 繪製流程直接使用 Skia,但依賴於原生的能力仍需非同步互動;
  • 相容性差:Android、iOS 各版本都存在各種相容性問題,特別是 Android 碎片化嚴重;
  • 問題排查成本高:跨端框架一般涉及 Native、FE、Server,中間做了大量的橋接轉換,排查鏈路比純 Native 長;
  • 動態化能力受限:相比純原生的外掛化,跨端框架動態更新的業務如果涉及 Native 部分的元件更新,需要依賴 App 發版。

不過,雖然跨平臺有自己的缺點,但是很多大型 App 都是會選擇它,至於是選擇 H5 + React Native + 佈局動態化,或者 H5 + Flutter,需要依據業務場景、包大小、效能、執行記憶體、動態更新能力為標準進行選取。

那如果從 0 到 1 搭建一個混合專案呢?

二、環境搭建

混合開發需要原生環境的支援,所以請確保本地已經配置了原生 Android 和 iOS 的開發環境。

2.1 Android 混合開發

2.1.1 建立 Android 工程

首先,使用 Android Studio 建立一個新的 App 專案,如果你已經有了本地專案,可以跳過此步驟。填寫完專案名稱、包名、專案本地路徑後,點選 “Finish” 按鈕。你可以把這個專案名稱取名為 “GeekTimeRNAndroid”。
在這裡插入圖片描述
在這裡插入圖片描述

2.1.2 新增 React Native 依賴

建立好本地工程後,我們就要給它新增依賴。其實,React Native 官方對整合到現有的原生應用提供了相應的 文件 。

按照官方文件的提示,我們需要“建立一個空目錄用於存放 React Native 專案,然後在其中建立一個 /android 子目錄,把你現有的 Android 專案拷貝到 /android 子目錄中”。

當然,官方提供的方式是非常不錯的,它偏向於 React Native 的工程管理模式。而我們在實際開發中,特別是已經上線的專案裡面,React Native 功能和其他業務功能一樣,一般會被當成原生工程的子模組來管理,所以我們這邊選擇偏向混合工程管理方式來整合。

下面是 RN 混合開發的幾種存在方式:
在這裡插入圖片描述

可以看到,對比官方的接入模式,原生混合 RN 的工程模式有以下好處:

  • 可以不侵入現有工程結構,React Native 模組會作為現有工程的一部分進行組織管理。
  • 不會影響程式碼倉庫管理,不用把 Android、iOS 放在同一程式碼倉庫下進行管理。
  • 混合模式方便我們進行元件功能複用,可以將 React Native 模組獨立成元件,提供給其他 App 平臺使用。

需要說明的是,使用上面的方式整合 React Native 模組時,需要新增 react-native 和 JavaScript 引擎的依賴。

  • react-native:React Native 的核心框架;
  • JavaScript 引擎:可選 JSC、Hermes,用於執行 JavaScript。

接下來,我們參考官方文件,新增 React Native 依賴,如下。

# 切換到剛剛新建好的工程目錄
cd /Users/Mac/RN/RNAndroid

# 執行新增 react-native 命令,yarn 或 npm 都可以
yarn add react-native

執行 yarn add react-native 命令後,預設會安裝最新版本的 React Native 包。執行以上命令成功之後,我們會發現 RNAndroid 工程下多了一個 node_modules 目錄,裡面不僅有 react-native 框架,還有 JavaScript 引擎編譯好的產物,包括 aar 和 pom 依賴檔案。接下來,我們可以參考官方提供的方式,將 node_modules 目錄配置為 repository,然後在工程中引入相關依賴。

不過,這種方式並不太推薦,其實我們只需要 react-native 和 JavaScript 引擎這兩個產物就可以了。獲取這兩個產物後,在 Android 自己進行二次封裝,然後釋出到公司的遠端倉庫。

接著開啟 node_module 中的 react-native 框架編譯產物,位於…/RNAndroid/node_modules/react-native目錄,如下圖。
在這裡插入圖片描述

node_module 中的 JSC 引擎編譯產物位於…/RNAndroid/node_modules/JavaScriptc-android目錄。
在這裡插入圖片描述

除此之外,新版本的 RN 還接入了 Hermes 引擎,node_module 中的 Hermes 引擎編譯產物位於…/RNAndroid/node_modules/hermes-engine目錄。
在這裡插入圖片描述

對於 JSC 引擎和 Hermes 引擎,我們需要注意以下兩點常識:

  • 在啟動效能上,Hermes 比 JSC 更快。Hermes 採用的是 AOT 提前編譯的方案,它支援位元組碼檔案,相比於 JSC,Hermes 不用先將 JavaScript 文字編譯為位元組碼,節約了編譯位元組碼的耗時,自然啟動效能更好。
  • 在執行效能上,JSC 比 Herems 更快。JSC 使用的是 JIT 即時編譯方案,該方案支援動態快取熱點程式碼,因此執行效能上更快。

但整體而言,由於 Hermes 引擎是專門為移動端定製的,在引擎大小、啟動速度、執行記憶體、通訊效能等方面都優於 JSC。

接下來,我們繼續被 RNAndroid 工程新增 react native 相關的依賴,包括:

  • react-native.arr 檔案;
  • react-native.aar 依賴的第三方庫;
  • JavaScript 引擎 aar 檔案。

首先,我們新增 react-native.arr 檔案。我們將上面的 react-native.arr 拷貝放置到 RNAndroid/libs 目錄下,然後新增依賴。

implementation fileTree(dir: 'libs',includes: ['*.jar','*.aar'])

implementation(name:'react-native-0.68.2', ext:'aar')

接著,我們再將 …/RNAndroid/node_modules/react-native/ 目錄下的 react-native-0.68.2.pom 中的依賴庫,按照 android gradle 依賴的方式進行新增,這些依賴主要是 react-native aar 本身遠端依賴的第三方庫。新增好的build.gradle 如下:

dependencies {
    implementation(name:'react-native-0.68.2', ext:'aar')
    
    implementation 'com.facebook.infer.annotation:infer-annotation:0.18.0'
    implementation 'javax.inject:javax.inject:1'
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'com.facebook.fresco:imagepipeline-okhttp3:2.5.0'
    implementation 'com.facebook.fresco:fresco:2.5.0'
    implementation 'com.facebook.soloader:soloader:0.10.3'
    implementation 'com.google.code.findbugs:jsr305:3.0.2'
    implementation 'com.squareup.okhttp3:okhttp:4.9.2'
    implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.9.2'
    implementation 'com.squareup.okio:okio:2.9.0'

    ... //省略其他依賴
}

接下來,我們繼續新增 JavaScript 引擎 aar 包。開啟 …/RNAndroid/node_modules/hermes-engine 目錄下的 hermes-cppruntime-release.aar & hermes-release.aar,然後拷貝到 libs 目錄,並在 build.gradle 中新增依賴。

dependencies {
   
    implementation(name:'android-jsc-r250230', ext:'aar')
    ... //省略其他依賴 
}

2.1.3 許可權配置

新增依賴後,接下來還需要進行許可權的配置,開啟混合工程的 AndroidManifest.xml 清單檔案中,然後新增網路許可權和訪問開發者選單。

<uses-permission android:name="android.permission.INTERNET" />

<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" />

至此,Android 混合工程中的 React Native 環境配置就已經完成了。接下來我們再看看,如何在 iOS 中進行 React Native 環境配置。

2.2 iOS 混合開發

2.2.1 建立 iOS 工程

首先,我們需要跟著 React Native 官方文件,在本地建立一個對應版本的 React Native 工程,這裡我們假設專案需要 0.68.2 版本。

# 首先安裝 react-native-cli, 否則無法使用 react-native 命令
sudo npm install -g react-native-cli

react-native init projectName --version 0.68.2

建立好後,我們再開啟工程,在工程 node_modules/react-native/template/ 目錄下執行 npm install,然後進入 /ios/ 目錄下執行 pod install,結束後再開啟 react native workspace 工程。

接下來,我們看一下如何在已有的 iOS 工程中接入 React Native。首先,你需要將以下三個 React Native 原始碼引入到 iOS 工程中,這三個原始碼分別為 Libraries、React,以及 React Common。
在這裡插入圖片描述

然後你將這三個部分作為 React Native 功能模組,直接參考官方提供的 podspec(參考: https://github.com/facebook/react-native ),並結合自己工程,選擇合理的接入方案。

2.2.2 新增 iOS 端庫依賴

接下來,我們需要修改 Podfile,來引用其他依賴的第三方庫,包括 DoubleConverison、glog、RCT_Folly、libevent 等。podspec 配置檔案則直接使用官方提供的檔案。

# Explicitly include Yoga if you are using RN >= 0.42.0
  pod "Yoga", :path => "../node_modules/react-native/ReactCommon/yoga"
  # Third party deps podspec link
  pod 'DoubleConversion', :podspec => '../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
  pod 'glog', :podspec => '../node_modules/react-native/third-party-podspecs/glog.podspec'
  pod 'Folly', :podspec => '../node_modules/react-native/third-party-podspecs/Folly.podspec'

具體可以參考純 RN 應用的 iOS 工程的 Podfile 檔案。然後,我們再執行 pod install 安裝依賴的外掛,並構建工程檔案 xcworkspace,並開啟工程檔案進行編譯就可以了。

可以看到,與 React Native 新專案不同,在已有 原生 Android 和 iOS 專案中接入 React Native,都是把 React Native 當成子模組然後再進行引入。不過,環境配置流程中,Android 側重於依賴 React Native 的框架 aar,以及 JavaScript 引擎 aar,而 iOS 則是使用原始碼方式整合 React Native 相關的依賴庫。

三、載體頁開發

新增完依賴之後,接下來我們還需要設計一個 React Native 載體頁。載體頁的作用是載入渲染 React Native 的容器,在 Android 中為 Activity/Fragment,在 iOS 中則為 ViewController。

以 Android 為例,首先我們新建一個 Activity,構建 ReactRootView,初始化 ReactInstanceManager 就可以載入本地的 bundle 檔案了。對於這部分餓程式碼,我們可以按照官方文件來快速搭建一個 React Native 載體頁,或者直接參考純 RN 專案的 Android 入口程式碼。

但在實際開發中,我們使用 React Native 跨框架,除了看中它跨平臺的優勢外,還特別在意它的熱更新能力。並且,為了進行 Bug 除錯和分析,需要具備錯誤處理、上報能力,以及複雜業務中,Native & JavaScript 通訊還需要提供通訊能力。甚至,根據業務需求,還需要提供一些通用內建元件、元件註冊能力等等。

所以,一個可用於商業上線的載體頁,需要提供初始化、載入渲染 React Native 頁面、熱更新、快取管理、生命週期管理、Native & JavaScript 通訊、錯誤處理、錯誤上報、內建基礎元件、元件註冊等能力。

3.1 整體設計

首先,我們看一下一個已經設計好的載體頁,架構圖如下。
在這裡插入圖片描述

可以看到,一個完整的 RN 載體頁,應該包含下面幾個部分:

  • UI 結構:在混合開發中,React Native 大部分以獨立頁面存在,載體頁可以包含通用標題欄和 React Native 容器。當然也可以直接暴露容器檢視,由使用方動態進行新增;
  • 對外能力:包含生命週期管理、Native 與 JavaScript 通訊能力、內建的基礎業務元件、元件註冊能力、異常回撥、錯誤上報等,同時還需要提供熱更新、載入 bundle 的能力;
  • 熱更新:請求遠端伺服器獲取對應 bundle 是否有最新版本。有最新版本則下載並執行載入,無最新版本則使用快取進行載入 (無快取則先下載),其中包含預載入、非同步更新等業務策略提升載入效能;
  • 快取管理:通常 bundle 包隨著業務體量增加,體積會越來越大。針對這種情況,我們的常用策略是拆包、按需載入,bundle 包在傳輸過程中會進行 zip 壓縮、加密,下載成功後進行解壓、校驗。每個 bundle 檔案都有對應的 id、版本號、content hash;
  • bundle 載入:JavaScript 引擎讀取 bundle 檔案,常用引擎包括 JSC、Hermes;
  • 執行環境:整個 React Native 執行環境包含負責渲染的 RootView,框架內建核心元件、業務定製元件,執行指令碼的 JavaScript 引擎,負責 Native 與 JavaScript 互動的 bridge/JSI。

3.2 初始化載體頁

有了載體頁的設計後,接下來就是如何初始化載體頁。這裡說的初始化,主要是 React Native 框架本身的初始化。接下來,我們依然分成 Android、iOS 兩端來分析。

3.2.1Android 載體頁初始化

為了方便後續的理解,我們首先看一下 React Native Android 中幾個常用類的作用:

  • ReactContext:繼承於 ContextWrapper,是 React Native 應用的上下文,管理著 CatalystInstance 以及三大執行緒 (UIThread、NativeModulesThread、JSThread);
  • ReactInstanceManager:總的管理類,管理 ReactPackage、ReactContext、ReactRootView、控制生命週期,同時還可以設定 JavaScript 引擎;
  • ReactRootView:React Native 渲染的原生容器,繼承於 FrameLayout;
  • CatalystInstance:Java 層、C++ 層、JavaScript 層通訊的總管理類,管理著 Java 層、JavaScript 層 Module 對映表與回撥,是三端通訊的橋樑。實現類為 CatalystInstanceImpl,支援向 JavaScript 注入全域性變數、動態載入指令碼檔案、獲取 NativeModules & JSModules;
  • JavaScriptModule:JS Module,負責 JavaScript 到 Java 的對映呼叫格式宣告,由 CatalystInstance 統一管理;
  • NativeModule:Java Module,負責 Java 到 JavaScript 的對映呼叫格式宣告,由 CatalystInstance 統一管理;
  • UIManager: 處理 UI 的渲染,JavaScript 層通過 C++ 層把建立 View 的請求傳送給 Java 層的 UIManagerModule。

React Native 初始化的核心是通過 ReactInstanceManagerBuilder 構建 ReactInstanceManager,而 ReactInstanceManager 管理著 ReactPackage、ReactContext、ReactRootView、控制生命週期等內容。官方提供了對 ReactInstanceManager 的構建說明,下面我們看一段程式碼(位於 ReactNativeHost.class),當然原始碼是使用的鏈式方式進行編寫的:

ReactInstanceManagerBuilder builder = ReactInstanceManager.builder();
// 設定 application 上下文
builder.setApplication((Application) context.getApplicationContext());

// 新增包,一個 package 由多個元件構成,上述程式碼中的 MainReactPackage 是 RN 內建的 package 
builder.addPackage(new MainReactPackage());

// JS 異常回撥處理實現,在這個實現中我們可以列印 JS 異常日誌,上報錯誤
builder.setRedBoxHandler(mRedBoxHandler); 

// native module 異常回撥處理實現,在這個實現中我們可以列印 native module 異常日誌,上報錯誤
builder.setNativeModuleCallExceptionHandler(mNativeModuleExceptionHandler);

// JS bundle 中主入口的檔名,demo 中 "index" 表示入口檔名為 index.js
builder.setJSMainModulePath("index");

// 是否開啟 dev 模式
builder.setUseDeveloperSupport(true);

// 設定建立時機
builder.setInitialLifecycleState(LifecycleState.BEFORE_CREATE); 

// 設定 JS 引擎,如果想使用 Hermes 引擎,可以這樣設定,需要引入 Hermes 相關的 so 庫
// builder.setJavaScriptExecutorFactory(new HermesExecutorFactory());

ReactInstanceManager reactInstanceManager = builder.build();

然後,我們要獲取 ReactContext,ReactContext 在後續載入渲染過程中會用到,程式碼 ReactNativeFlipper.class 中,如下。

reactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() {
    @Override
    public void onReactContextInitialized(ReactContext reactContext) {
       mReactContext = reactContext;
    }
});

至此,Android 端的初始化工作就完成了,接著我們來看下 iOS 端的初始化邏輯。

3.2.2 iOS 載體頁初始化

在 iOS 載體頁初始化流程中,我們首先需要建立一個 Bridge。在 React Native 中,通過 Bridge 實現了 JavaScript 與原生框架之間的通訊,呼叫 React Native 提供的 API,就相當於通過 Bridge 呼叫原生的 API。因此,初始化載體頁的第一步就是建立一個 Bridge,與載體頁一對一繫結。

RCTBridge *carrierBridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil];

接下來,我們需要建立一個 RCTRootView,用於展示 React Native 檢視的 RCTRootView 元件,在 JavaScript 程式碼中 render() 部分的 UI 元件均會渲染到該 View 中,建立方式如下。

RCTRootView *rctView = [[RCTRootView alloc] initWithBridge:bridge 
moduleName:moduleName initialProperties:nil];
[self.view addSubview:rctView];

到此,iOS 端初始化準備過程就完成了,是不是很簡單。

那麼接下來,我們就需要獲取 React Native 程式碼包,也就是 JS Bundle 資源。那麼我們要怎麼動態下載 JS Bundle 資源呢?我們可以採用熱更新策略,動態下載 JS bundle 資源。每個不同的 JS bundle 包都有它自己的標識 id,我們可以根據該 id 從伺服器中獲取該 JS bundle 資源對應的最新版本號,以及最新資源下載地址。

在獲取 JS Bundle 的最新版本號後,如果使用者之前瀏覽過當前 React Native 頁面,還存在快取,那麼我們就可以檢測快取版本號是否與最新版本號相同。如果是相同的,就不需要重複下載了;如果不相同,那你還要根據最新資源下載地址,下載最新的資源包,並快取到本地,如下圖。

在這裡插入圖片描述

通過以上步驟,我們就能建立好載體頁併成功下載 JS bundle 了,那麼現在就可以準備開始執行 JavaScript 程式碼並渲染 React Native 頁面了。

3.2.3 載入 JS bundle

Android 載入 bundle 流程

Android 端通過 ReactContext 獲取 CatalystInstance 物件,CatalystInstance 實現類為 CatalystInstanceImp。CatalystInstanceImpl 有一個非 public 方法,即 loadScriptFromFile(),我們通過這個方法就可以動態載入本地的 bundle 檔案了。不過,由於 loadScriptFromFile() 為非 public,所以需要反射獲取呼叫。程式碼如下:

CatalystInstance catalystInstance = mReactContext.getCatalystInstance();
Method loadScripFromFileMethod = CatalystInstanceImpl.class.getDeclaredMethod("loadScriptFromFile", String.class, String.class, boolean.class);
loadScripFromFileMethod.setAccessible(true);
// fileName 和 sourceURL 傳入本地快取的 bundle 路徑,loadSynchronously 為是否同步載入
loadScripFromFileMethod.invoke(catalystInstance, fileName, sourceURL, loadSynchronously);

接著,我們再呼叫 ReactRootView 的 startReactApplication()方法就可以開始載入渲染 React Native 頁面了。這裡要注意,startReactApplication() 中的引數 moduleName 必須對應 “index.js” 中的 “AppRegistry.registerComponent()” 的第一個引數,如下。

reactRootView.startReactApplication(reactInstanceManager, moduleName, launchOption);

到此,Android 端動態載入 bundle 就講完了,我們繼續來看 iOS 載入 bundle 的流程。

iOS 載入 bundle 流程

在 iOS 載入 bundle 的流程中,我們需要先初始化 Bridge,然後才執行載入 JavaScript 包。接下來,我們結合原始碼來看一下 iOS 載入 bundle 的整體流程。

首先,在初始化 Bridge 時,在 setup 的過程中,首先會呼叫 bridge 的代理方法 (NSURL *)sourceURLForBridge : (RCTBridge *)bridge 方法,指定獲取 JS bundle 的路徑。

 -(NSURL *)sourceURLForBridge:(RCTBridge *)bridge{
    NSString *bundlePath = [self getCurrentBundlePath:bundleid];
    return bundlePath;
}

確定 URL 之後,bridge 就開始呼叫 start() 方法,開始載入 JS bundle 並呼叫以下方法:

     [self loadSource:^(NSError *error, RCTSource *source) {
    if (error) {
      [weakSelf handleError:error];
    }
    ...
 ]

接下來,呼叫 bridge 的代理方法,我們可以在該方法中手動注入一些業務引數:

- (void)loadSourceForBridge:(RCTBridge *)bridge
                 onProgress:(RCTSourceLoadProgressBlock)onProgress
                 onComplete:(RCTSourceLoadBlock)loadCallback{
  [RCTJavaScriptLoader loadBundleAtURL:bridge.bundleURL onProgress:onProgress onComplete:^(NSError *error, RCTSource *source) {
      //手動注入一些業務引數
      NSString *string = ";this.__xxx___ = 'yyy';"
      NSData *stringData = [string dataUsingEncoding:NSUTF8StringEncoding];
      
      NSMutableData *newData = [NSMutableData dataWithData:stringData];
      [newData appendData:source.data];
      
      //生成新的 Source 去載入
      RCTSource * newSource = [RCTJavaScriptLoader getNewRCTSourceURL:source.url data:newData];
      loadCallback(error,newSource);
  }];
}

之後,bridge 會負責執行 JavaScript 程式碼並渲染出頁面。

四、常見問題及改造

4.1 Android 問題排查

在軟體開發中,只要有足夠多的日誌,就能幫我們足夠快地定位問題。所以對於應用開發,特別是這種跨平臺的應用開發,我們首先要做的就是新增日誌輸出。然後,我們可以通過 ReactInstanceManagerBuilder 獲取 JavaScript、native 執行錯誤。

ReactInstanceManagerBuilder builder = ReactInstanceManager.builder();
builder.setApplication((Application) context.getApplicationContext())
       .setRedBoxHandler(mExceptionHandler)
       .setNativeModuleCallExceptionHandler(mExceptionHandler);
        
private static class ExceptionHandler implements NativeModuleCallExceptionHandler, RedBoxHandler {

  @Override
  public void handleException(Exception e) {
    // 處理 Native 異常
  }

  @Override
  public void handleRedbox(String s, StackFrame[] stackFrames, ErrorType errorType) {
    // 處理 JS 異常
  }
  
  @Override
  public boolean isReportEnabled() {
    return false;
  }
  
  @Override
  public void reportRedbox(Context context, String s, StackFrame[] stackFrames, String s1, ReportCompletedListener reportCompletedListener) {
  }
}

接著,我們是有 AOP 方式攔截 React Native 的 JavaMethodWrapper 呼叫,並使用 AspectJ 在編譯期對 ReactNative 框架位元組碼進行插樁:

@Aspect
public class NativeModuleMethodInvokeAspect extends BaseAspect<INativeModuleMethodInvokePointcut> {

    // 對 JavaMethodWrapper.invoke 方法呼叫進行插樁
    @Around("execution (* com.facebook.react.bridge.JavaMethodWrapper.invoke(..))")
    public Object invokeNativeModuleMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        INativeModuleMethodInvokePointcut pointcut = getPointcut();
        if(pointcut == null){
            return joinPoint.proceed(joinPoint.getArgs());
        }
        return pointcut.pointcut(joinPoint);
    }
}

下面這段程式碼是針對 Native Module 的呼叫做程式碼插樁,捕獲 JavaScript 呼叫 Native Module 的異常,如下。

public class ModuleMethodInvokePointcut implements INativeModuleMethodInvokePointcut {
    @Override
    public Object pointcut(ProceedingJoinPoint proceedingJoinPoint) {
        Object object;
        Object target = proceedingJoinPoint.getTarget();
        try {
            object = proceedingJoinPoint.proceed();
        } catch (Throwable throwable) {
            // 異常時,通過 target 反射獲取對應的 module 名稱和方法
            throwable.printStackTrace();
            return null;
        }
        return object;
    }
}

然後,我們就可以通過下面這兩個命令檢視 React Native 執行日誌了。

adb logcat | grep React

最後,如果有異常日誌就會上報到後臺,日誌資訊會包含系統、機型、執行環境,以及業務標識等。有了異常日誌給我們提供的這些資訊,你就可以通過線下、線上手段,快速排查 React Native 相關的 bug 了。

那麼接下來,我們對 React Native 框架的 Bug 進行修復呢?在 Android 混合工程中,React Native 是以 aar 方式引入的。所以建議你對它進行包裝,自行釋出 aar 管理。不過原始碼編譯方式複雜,你可以通過編譯期插樁的方式對 React Native aar 進行插樁,來修復 bug。

常用的插樁方式包括 ASM、Javasist、AspectJ 這幾種。我們通常可以用 AspectJ 匹配具體類的具體方法進行插樁,實現 bug 修復。選擇 AspectJ 的原因是,通常我們只需要對 React Native 的一些異常方法做切片,並不需要修改裡面的邏輯,它足以滿足我們修改 React Native 框架問題的需要。並且,AspectJ 在易用性、可讀性上比 ASM、Javasist 都更優。

4.2 iOS 問題排查

在 iOS 混合專案中,如果 React Native 頁面在載入過程中或者執行過程中出現了異常,我們可以統一通過下面的方法來進行攔截。

typedef void (^RCTFatalHandler)(NSError *error); 

首先,我們要對原生的 RCTFatalHandler 中 error 引數進行改造,讓 error 中帶上 bridge 資訊。

- (void)error{
    //改造 error 中帶有 bridge
    NSError *newError = [self getWBErrorBridge:error];
    RCTFatal(newError);
}

//error 資訊裡帶上 Bridge
- (NSError *)getWBErrorBridge:(NSError *)error{
    NSMutableDictionary *errorDic = [NSMutableDictionary dictionaryWithDictionary:error.userInfo];
    [errorDic setObject:self->_parentBridge forKey:@"ErrorBridgeTag"];
    NSError *newErr = [[NSError alloc]initWithDomain:error.domain code:error.code userInfo:errorDic];
    return newErr;
}

出現異常後,我們要從 Error 中獲取 bridge,並找出發生異常的載體頁資訊,獲取對應的 JS Bundle 的 ID,以確定到底哪一個頁面出現了異常。

RCTSetFatalHandler(^(NSError *error) {
    dispatch_async(dispatch_get_main_queue(), ^{
        RNViewController *rnVC = nil;
        RCTBridge *bridge = (RCTBridge *)error.userInfo[@"ErrorBridgeTag"];
        if (bridge) {
           carrierVC = (RNViewController *)bridge.delegate;
        }
        NSString *descriptions = error.localizedDescription;
        NSLog(@"error --- %@ --- %@", rnVC.rnModel.bundleID, descriptions);
    }
}

經攔截異常後,我們就可以對頁面進行展示錯誤頁面、自動修復、清空異常的 JS bundle 快取等一系列操作了。

五、總結

本文主要講了原生和 RN 混合開發的流程,讀者可以根據官方文件快速搭建起 React Native 混合工程。除此之外,我們還從環境搭建、載體頁設計、除錯打包釋出、問題排查與框架 Bug 修復幾個方面來進行講解的。

總的來說,如果是一個新的專案,需要做到快速提效,並且不需要複雜的架構,這個時候你可以選擇純 React Native 模式。如果是已上線專案來接入 React Native,架構複雜,或者需要將 React Native 當成一種基礎能力提供給其他業務 /App 使用,就需要使用混合模式。

接著,我們介紹了在遇到問題時如何快速排查定位,是我們實際開發過程中經常遇到的問題。由於 React Native 的鏈路比較長、涉及客戶端、前端、後端,且 React Native 框架輸出的日誌不夠多,排查問題比較困難。這個時候我們可以通過捕獲 React Native 執行異常、在 React Native 框架內加入足夠的日誌,幫助我們來快速定位問題。

相關文章