在 ReactNative 的 App 中,整合 Bugly 你會遇到的一些坑

承香墨影發表於2019-02-22

一、前言

最近開新專案,準備嘗試一下 ReactNative,所以前期做了一些調研工作,ReactNative 的優點非常的明顯,可以做到跨平臺,除了少部分 UI 效果可能需要對不同的平臺進行單獨適配,其中的核心邏輯程式碼,都是可以重用的。所以如果最終用 ReactNative 的話,可以省出某一端的客戶端開發人員。而我這裡調研的主要方向,就是它對國內第三方 SDK 的支援。

在國內,開發 App,一般都是會整合一些第三方服務的,例如:升級、崩潰分析、資料統計等等。而這些第三方服務,提供的 SDK ,通常只有 Native 層的,例如 Android 就是使用 Java 寫的。而 ReactNative 本身 JavaScript 和 Native 層(Java層)的通訊,其實已經做的很好了,所以大部分情況下,我們只需要對這些 SDK 做一個簡單的封裝就可以正常使用它了。

本期就來分享一下,如何在 ReactNative 的基礎之上,整合 Bugly。這裡主要是看它的崩潰蒐集,這也是 Bugly 的主要功能。對於崩潰的收集,我主要關心兩個部分:

  1. 是需要統計到正確的崩潰棧。
  2. 統計到的崩潰棧要是易於閱讀的。

其實主要工作卡在了後者,接下來讓我們具體看看問題。

本文的分析都是基於最新的 ReactNative (v0.49) 版本來分析。

二、ReactNative 的崩潰統計

首先,ReactNative 中 JavaScript 和 Native 層的通訊,官方文件已經寫的非常清楚了。在官方文件中,舉了一個 Toast 模組的例子,寫的很清晰,這裡就不再贅述了,還不瞭解的,可以先看看文件。

ReactNative 原生模組(中文文件):

reactnative.cn/docs/0.49/n…

2.1 ReactNative 的編譯模式

而在 ReactNative 的程式中,實際上執行的是 Js 的程式碼,而它也是分 Debug 和 Release 的。

在 Debug 模式下,會從本地開啟一個 Packager 服務,然後 App 執行起來之後,直接從服務里拉取最新的編譯後的 JS 程式碼,這樣可以在開發階段,做到程式碼實時更新的效果,只需要在裝置上,重新 Load 一下即可。

而在 Release 模式下,ReactNative 會將 JS 程式碼,整體打包,然後放到 assets 目錄下,然後從這裡去載入 JS 程式碼。

這樣的邏輯被封裝在 ReactInstanceManager 類的 recreateReactContextInBackgroundInner() 方法中,有興趣可以自行看看。

react-server
react-server

可以很清晰的看到,在 Debug 和 Release ,分別使用的不同的方式,載入 JS 檔案的。這裡為什麼要說到 ReactNative App 的編譯模式呢?其實和後面的邏輯有關係。

ReactNative 在 Debug 的情況下,其實還是很貼心的,如果出現崩潰的 Bug,會直接出紅屏,提示你崩潰的棧的具體資訊,這些內容可以幫助你快速的定位問題。

js-crash
js-crash

這裡給的例子,是一個 Js 層的崩潰,可以看到它崩潰棧中,很清晰的看到 App.js 檔案的第 48 行 21列,會有一個 ReferenceError 的錯誤。

最方便的是,你直接點選崩潰棧的程式碼,會自動開啟對應的 Js 檔案。當然,如果是一個 Native 層的崩潰,雖然也會出紅屏,但是點選並不能跳轉。

而假如現在同樣的程式碼,使用 Release 模式的話,則會直接崩潰了。

2.2 不同編譯模式的 Js 有什麼不同

假如 Release 和 Debug 一樣,可以有如此清晰的崩潰棧,其實問題就已經得到解決。但是當你使用 Release 包來觸發一個崩潰的時候,你就會發現,它並不是一樣的。

使用命令,可以直接安裝一個 Release 版本到裝置上。

cd android && ./gradlew installRelease複製程式碼

這裡其實是兩行命令,先進入到 android 專案的目錄,然後執行 ./gradlew installRelease 這個沒什麼好說的,如果執行失敗,注意一下當前 shell 環境的目錄路徑。

此時,我們再執行它就會直接導致崩潰,來看看崩潰的 Log 輸出。

js-crash-stack
js-crash-stack

很尷尬的是,雖然崩潰棧也被輸出出來了,和前面紅屏的截圖對比一下,也能發現它們其實是一個內容。但是,這些程式碼被混淆過了,如果 Native App 一樣,混淆過的程式碼,反編譯來看會變成 a.b.c ,這裡的效果也是類似的。

這樣的崩潰棧,其實拿出來,可讀性非常的差,但是並不是不可讀的。

那麼接下來來看看如何定位到這個崩潰的真實程式碼,value@304:1133 這裡,就是線索。我們把 Apk 解壓,拿到其內 assets/index.android.bundle 檔案,它其內就是我們 ReactNative 編譯好的 Js 檔案,可以看到它的第 304 行 1133 列,就是我們需要定位的出了問題的程式碼。

index-bundle
index-bundle

這樣的編譯後的程式碼,查 Bug 查起來就非常的費時了,你首先需要根據當前版本釋出出去的 Apk,然後根據其中的 index.android.bundle 檔案,定位到具體的程式碼,之後再結合上下文全文搜尋你的原始碼,才能找到對應出錯的程式碼。

注意我這裡本身專案就是一個 Demo 專案,程式碼量比較少,還能準確的定位到問題,如果是一個實際的專案,在打 Release 包的時候,會將所有的 JS 檔案全部打包到 index.android.bundle 檔案中去。在這個例子中,如果 props.username.name 這段程式碼,我在很多地方都用到的話,篩選它也是非常麻煩的。

2.3 Release 缺少了什麼?

從前面的內容可以瞭解到,Release 包同樣也是可以定位到出錯的程式碼的。但是,你依然需要全文的搜尋這段程式碼,無法精準定位到具體出錯程式碼所在的原始檔,這是為什麼?

Release 包的 Js 一定是經過混淆的,會剝離掉一些必要的資訊,這些被剝離的資訊,導致我們無法精準定位到程式碼的原始檔上。

在 Debug 模式下,執行我們的 Packager Server ,然後在瀏覽器中訪問:

http://localhost:8081/index.android.bundle?platform=android&dev=true

請確保你的 Packager Server 保持執行的情況下訪問。

就可以看到當前 Debug 模式,App 所執行的 JS 程式碼。我們直接根據出錯程式碼,精準定位一下。

debug-server
debug-server

在這裡,就可以很清晰的看到,它有一個 fileName 和 lineNumber 兩個屬性,分別用來記錄當前原始碼的檔案和這段程式碼所在的行數。而回憶一下之前 Release 版本的 JS 程式碼,你會發現關於原始檔和行號的資訊,被剝離了。

這也就是我們無法精準定位出錯程式碼和鎖在原始檔的根本原因。

2.4 Mapping

既然已經明確的知道,在 Release 下,會過濾掉一些關於原始檔和行號的資訊,就如同 Android 的混淆一樣,那它是否包含類似對照關係的 Mapping 檔案,可以幫助我們還原回去?

那麼我們就需要找到 index.android.bundle 這個檔案,是如何產生的。

ReactNative App 的打包,完全藉助了 react.gradle 這個檔案,你可以在 Android 工程的 build.gradle 檔案中找到它。

app-gradle
app-gradle

繼續最終 node/modules 下的 react.gradle 檔案。

react-gradle
react-gradle

可以看到它實際上是通過 react-native bundle 命令,通過增加引數的形式,輸出 index.android.bundle 檔案的。

而如果你查閱文件,你會發現 react-native 命令,還有一個可配置的引數 —sourcemap-output,它就是我們需要的。

完整的說明,你可以在這個網站上找到資料:

docs.bugsnag.com/platforms/r…

我這裡把關鍵資訊截圖出來看著更清晰。

source-map
source-map

--sourcemap-output 命令非常的簡單,只需要配置一個輸出的檔名就可以了。

這裡我們直接在命令列裡執行如下程式碼,就可以自動重新生成一個 index.android.bundle 檔案,並且同時也會生產一個對應關係的 map 檔案。

react-native bundle 
--platform android 
--dev false 
--entry-file index.js 
--bundle-output android/app/src/main/assets/index.android.bundle 
--assets-dest android/app/src/main/res/ 
--sourcemap-output android-release.bundle.map複製程式碼

執行效果如下:

build-map
build-map

注意這段命令,需要在 ReactNative 目錄的根目錄下執行,否者會提示你找不到 node_module 。執行完成,就可以在 ReactNative 專案目錄下,看到輸出的 android-release.bundle.map 檔案了。

點開看看,完全看不懂,隨便截個圖讓大家感受一下。

map
map

其實到這裡,已經離我們的答案,更近一步了,Android 混淆的 Mapping 檔案,也不是我們肉眼能清晰看懂的,我們接下來只需要找到它的解析規則就可以了。

解析這個 source-map ,NodeJs 為我們提供了一個專門的庫來解析,這裡不多解釋,直接上程式碼。

map-js
map-js

/**
 * Created by cxmyDev on 2017/10/31.
 */
var sourceMap = require('source-map');
var fs = require('fs');

fs.readFile('../android-release.bundle.map', 'utf8', function (err, data) {
    var smc = new sourceMap.SourceMapConsumer(data);

    console.log(smc.originalPositionFor({
        line: 304,
        column: 1133
    }));
});複製程式碼

注意看這裡指定的 304 行 1133 列,我們執行一下,看看輸出。

map-js-output
map-js-output

這段程式碼,會很清晰的輸出對應的原始檔名和行號,以及錯的欄位,還是很清晰的。

再來對照我們的原始碼驗證一下。

error-line
error-line

確實也如 map.js 指令碼輸出的一樣。

2.5 小結

到此,我們算是完成了 ReactNative App,崩潰分析的一個完整的鏈路邏輯,我們只需要自己寫個指令碼工具,就可以幫我們精準定位了。

前面有點長,這裡總結一下本小結的內容。

  1. ReactNative 不同的編譯模式,使用的 JS 來源不同。Debug 模式來自 Packager Server,而 Release 模式,來自 Apk 的 assets 目錄。
  2. Debug 模式下的崩潰,會觸發紅屏,而 Release 模式下的崩潰,會直接導致 App 崩潰。
  3. Debug 模式,之所以可以顯示崩潰棧的基本資訊,是因為編譯的 JS 檔案中,包含了對應的原始檔和程式碼行號。而這些在 Release 模式下的 JS 是沒有的。
  4. Release 模式的崩潰棧是被混淆後的,可以通過崩潰棧顯示的行號和列號,來定位程式碼,但是無法定位具體原始檔。
  5. 通過 react-native 命名,增加 --sourcemap-output引數,指定輸出需要的混淆 Mapping 檔案,它其內包含了混淆的資訊。
  6. 解讀 ReactNative Mapping 檔案,可以使用 source-map 這個 NodeJs 庫來進行解析,可以精準定位到行號和原始檔名。

三、整合 Bugly 的坑

Bugly 的整合,非常的簡單。如果之前用過 Bugly 的,並且閱讀 ReactNative 和 原生通訊 這部分文件的話,差不多十分鐘就可以整合完畢。

還不瞭解 ReactNative 和原生通訊內容的,建議先閱讀一下本文件瞭解一下。

ReactNative 原生模組(中文文件):

reactnative.cn/docs/0.49/n…

Bugly 的註冊沒有什麼門檻,這裡直接使用個人 QQ 號就可以登入,建立一個專門為 ReactNative 測試的 App,然後根據文件繫結對應的 AppID 即可。

不清楚的可以查閱 Bugly 的文件:

bugly.qq.com/docs/user-g…

這部分內容沒什麼好說的,都是標準話的流程。接下來我們來看看整合它將面臨的坑。

3.1 Debug 模式下不會上報崩潰

之前也提到,Debug 模式下,如果觸發了崩潰,會直接進入紅屏狀態,顯示當前崩潰棧的資訊。這個功能,在我們開發階段,非常的好用,能快速定位問題。

但是正是因為 ReactNative 會在 Debug 模式下,Hook 住我們的崩潰棧,從而會導致 Bugly SDK 無法蒐集到對應的崩潰也就無法進行上報。

所以,如果你在 ReactNative 專案內,整合了 Bugly 之後。造的崩潰沒有得到上報,檢查一下自己編譯模式,一定要切換到 Release 模式下。

3.2 崩潰資訊整合

Bugly 為了方便開發者檢視,會將類似崩潰棧的崩潰,整合成一個,然後進行計數統計,只顯示當前崩潰了多少次和影響的人數。

而在 ReactNative 專案中,如果是 Native 層出現的崩潰,那其實沒有什麼差別,崩潰資訊和我們平時開發常規 App 一樣。

但是,如果這個崩潰是發生在 Js 層的話,它最終會把崩潰拋到 Native 層,同樣也是可以統計的的。但是這些崩潰會被封裝成一個 JavascriptException 丟擲來,從而導致它們被簡單的歸為了 JavascriptException 。可能它們描述的是不同的 Bug,但是卻被歸位一類,這樣之後查閱起來,就需要人工進行篩選。

這裡看兩個崩潰,第一個發生在 Js 層,第二個發生在 Native 層。

bugly-crash
bugly-crash

3.3 解讀 Bugly 中,js層的崩潰

Native 層的崩潰,和常規 App 一樣,沒什麼好說的。這裡只看 Js 層的崩潰資訊。

js-stack
js-stack

從這個崩潰棧你可以發現,其實下面 Java 的棧,基本上沒有任何資訊。這裡主要是閱讀上面 TypeError 後面的資訊。這裡描述了 Js 層崩潰的所有資訊,包含錯誤和崩潰棧。

前面的內容如果認真看了,應該不難發現此處就是對 JS 崩潰輸出的格式化拉平成一行了,所以如果我們要針對 Bugly 的崩潰棧編寫解析指令碼,就需要考慮到這些情況。

四、總結

本文說是 ReactNative 整合 Bugly 的一些坑,實際上講的更多的是在生產環境下,如何分析 ReactNative 的崩潰棧。這些被蒐集的原始資訊,如何被還原成我們需要的資訊。

不過這些,還是期待國內環境下,更多第三方 SDK 能支援到 ReactNative,畢竟官方團隊支援的肯定要比我們自己寫補丁指令碼來的方便實用。

今天在承香墨影公眾號的後臺,回覆『成長』。我會送你一些我整理的學習資料,包含:Android反編譯、演算法、Linux、虛擬機器、設計模式、Web專案原始碼。

推薦閱讀:

相關文章