一、前言
最近開新專案,準備嘗試一下 ReactNative,所以前期做了一些調研工作,ReactNative 的優點非常的明顯,可以做到跨平臺,除了少部分 UI 效果可能需要對不同的平臺進行單獨適配,其中的核心邏輯程式碼,都是可以重用的。所以如果最終用 ReactNative 的話,可以省出某一端的客戶端開發人員。而我這裡調研的主要方向,就是它對國內第三方 SDK 的支援。
在國內,開發 App,一般都是會整合一些第三方服務的,例如:升級、崩潰分析、資料統計等等。而這些第三方服務,提供的 SDK ,通常只有 Native 層的,例如 Android 就是使用 Java 寫的。而 ReactNative 本身 JavaScript 和 Native 層(Java層)的通訊,其實已經做的很好了,所以大部分情況下,我們只需要對這些 SDK 做一個簡單的封裝就可以正常使用它了。
本期就來分享一下,如何在 ReactNative 的基礎之上,整合 Bugly。這裡主要是看它的崩潰蒐集,這也是 Bugly 的主要功能。對於崩潰的收集,我主要關心兩個部分:
- 是需要統計到正確的崩潰棧。
- 統計到的崩潰棧要是易於閱讀的。
其實主要工作卡在了後者,接下來讓我們具體看看問題。
本文的分析都是基於最新的 ReactNative (v0.49) 版本來分析。
二、ReactNative 的崩潰統計
首先,ReactNative 中 JavaScript 和 Native 層的通訊,官方文件已經寫的非常清楚了。在官方文件中,舉了一個 Toast 模組的例子,寫的很清晰,這裡就不再贅述了,還不瞭解的,可以先看看文件。
ReactNative 原生模組(中文文件):
2.1 ReactNative 的編譯模式
而在 ReactNative 的程式中,實際上執行的是 Js 的程式碼,而它也是分 Debug 和 Release 的。
在 Debug 模式下,會從本地開啟一個 Packager 服務,然後 App 執行起來之後,直接從服務里拉取最新的編譯後的 JS 程式碼,這樣可以在開發階段,做到程式碼實時更新的效果,只需要在裝置上,重新 Load 一下即可。
而在 Release 模式下,ReactNative 會將 JS 程式碼,整體打包,然後放到 assets 目錄下,然後從這裡去載入 JS 程式碼。
這樣的邏輯被封裝在 ReactInstanceManager 類的 recreateReactContextInBackgroundInner()
方法中,有興趣可以自行看看。
可以很清晰的看到,在 Debug 和 Release ,分別使用的不同的方式,載入 JS 檔案的。這裡為什麼要說到 ReactNative App 的編譯模式呢?其實和後面的邏輯有關係。
ReactNative 在 Debug 的情況下,其實還是很貼心的,如果出現崩潰的 Bug,會直接出紅屏,提示你崩潰的棧的具體資訊,這些內容可以幫助你快速的定位問題。
這裡給的例子,是一個 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 輸出。
很尷尬的是,雖然崩潰棧也被輸出出來了,和前面紅屏的截圖對比一下,也能發現它們其實是一個內容。但是,這些程式碼被混淆過了,如果 Native App 一樣,混淆過的程式碼,反編譯來看會變成 a.b.c ,這裡的效果也是類似的。
這樣的崩潰棧,其實拿出來,可讀性非常的差,但是並不是不可讀的。
那麼接下來來看看如何定位到這個崩潰的真實程式碼,value@304:1133
這裡,就是線索。我們把 Apk 解壓,拿到其內 assets/index.android.bundle
檔案,它其內就是我們 ReactNative 編譯好的 Js 檔案,可以看到它的第 304 行 1133 列,就是我們需要定位的出了問題的程式碼。
這樣的編譯後的程式碼,查 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 程式碼。我們直接根據出錯程式碼,精準定位一下。
在這裡,就可以很清晰的看到,它有一個 fileName 和 lineNumber 兩個屬性,分別用來記錄當前原始碼的檔案和這段程式碼所在的行數。而回憶一下之前 Release 版本的 JS 程式碼,你會發現關於原始檔和行號的資訊,被剝離了。
這也就是我們無法精準定位出錯程式碼和鎖在原始檔的根本原因。
2.4 Mapping
既然已經明確的知道,在 Release 下,會過濾掉一些關於原始檔和行號的資訊,就如同 Android 的混淆一樣,那它是否包含類似對照關係的 Mapping 檔案,可以幫助我們還原回去?
那麼我們就需要找到 index.android.bundle 這個檔案,是如何產生的。
ReactNative App 的打包,完全藉助了 react.gradle 這個檔案,你可以在 Android 工程的 build.gradle 檔案中找到它。
繼續最終 node/modules 下的 react.gradle 檔案。
可以看到它實際上是通過 react-native bundle
命令,通過增加引數的形式,輸出 index.android.bundle 檔案的。
而如果你查閱文件,你會發現 react-native
命令,還有一個可配置的引數 —sourcemap-output
,它就是我們需要的。
完整的說明,你可以在這個網站上找到資料:
我這裡把關鍵資訊截圖出來看著更清晰。
--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複製程式碼
執行效果如下:
注意這段命令,需要在 ReactNative 目錄的根目錄下執行,否者會提示你找不到 node_module 。執行完成,就可以在 ReactNative 專案目錄下,看到輸出的 android-release.bundle.map 檔案了。
點開看看,完全看不懂,隨便截個圖讓大家感受一下。
其實到這裡,已經離我們的答案,更近一步了,Android 混淆的 Mapping 檔案,也不是我們肉眼能清晰看懂的,我們接下來只需要找到它的解析規則就可以了。
解析這個 source-map ,NodeJs 為我們提供了一個專門的庫來解析,這裡不多解釋,直接上程式碼。
/**
* 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
指令碼輸出的一樣。
2.5 小結
到此,我們算是完成了 ReactNative App,崩潰分析的一個完整的鏈路邏輯,我們只需要自己寫個指令碼工具,就可以幫我們精準定位了。
前面有點長,這裡總結一下本小結的內容。
- ReactNative 不同的編譯模式,使用的 JS 來源不同。Debug 模式來自 Packager Server,而 Release 模式,來自 Apk 的 assets 目錄。
- Debug 模式下的崩潰,會觸發紅屏,而 Release 模式下的崩潰,會直接導致 App 崩潰。
- Debug 模式,之所以可以顯示崩潰棧的基本資訊,是因為編譯的 JS 檔案中,包含了對應的原始檔和程式碼行號。而這些在 Release 模式下的 JS 是沒有的。
- Release 模式的崩潰棧是被混淆後的,可以通過崩潰棧顯示的行號和列號,來定位程式碼,但是無法定位具體原始檔。
- 通過 react-native 命名,增加
--sourcemap-output
引數,指定輸出需要的混淆 Mapping 檔案,它其內包含了混淆的資訊。 - 解讀 ReactNative Mapping 檔案,可以使用 source-map 這個 NodeJs 庫來進行解析,可以精準定位到行號和原始檔名。
三、整合 Bugly 的坑
Bugly 的整合,非常的簡單。如果之前用過 Bugly 的,並且閱讀 ReactNative 和 原生通訊 這部分文件的話,差不多十分鐘就可以整合完畢。
還不瞭解 ReactNative 和原生通訊內容的,建議先閱讀一下本文件瞭解一下。
ReactNative 原生模組(中文文件):
Bugly 的註冊沒有什麼門檻,這裡直接使用個人 QQ 號就可以登入,建立一個專門為 ReactNative 測試的 App,然後根據文件繫結對應的 AppID 即可。
不清楚的可以查閱 Bugly 的文件:
這部分內容沒什麼好說的,都是標準話的流程。接下來我們來看看整合它將面臨的坑。
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 層。
3.3 解讀 Bugly 中,js層的崩潰
Native 層的崩潰,和常規 App 一樣,沒什麼好說的。這裡只看 Js 層的崩潰資訊。
從這個崩潰棧你可以發現,其實下面 Java 的棧,基本上沒有任何資訊。這裡主要是閱讀上面 TypeError 後面的資訊。這裡描述了 Js 層崩潰的所有資訊,包含錯誤和崩潰棧。
前面的內容如果認真看了,應該不難發現此處就是對 JS 崩潰輸出的格式化拉平成一行了,所以如果我們要針對 Bugly 的崩潰棧編寫解析指令碼,就需要考慮到這些情況。
四、總結
本文說是 ReactNative 整合 Bugly 的一些坑,實際上講的更多的是在生產環境下,如何分析 ReactNative 的崩潰棧。這些被蒐集的原始資訊,如何被還原成我們需要的資訊。
不過這些,還是期待國內環境下,更多第三方 SDK 能支援到 ReactNative,畢竟官方團隊支援的肯定要比我們自己寫補丁指令碼來的方便實用。
今天在承香墨影公眾號的後臺,回覆『成長』。我會送你一些我整理的學習資料,包含:Android反編譯、演算法、Linux、虛擬機器、設計模式、Web專案原始碼。
推薦閱讀: