一、前言
最近開新項目,準備嘗試一下 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 原生子產品(中文文檔):
http://reactnative.cn/docs/0.49/native-modules-android.html#content
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
,它就是我們需要的。
完整的說明,你可以在這個網站上找到資料:
https://docs.bugsnag.com/platforms/react-native/showing-full-stacktraces/
我這裡把關鍵資訊截圖出來看着更清晰。
--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 命名,增加
參數,指定輸出需要的混淆 Mapping 檔案,它其内包含了混淆的資訊。--sourcemap-output
- 解讀 ReactNative Mapping 檔案,可以使用 source-map 這個 NodeJs 庫來進行解析,可以精準定位到行号和源檔案名。
三、內建 Bugly 的坑
Bugly 的內建,非常的簡單。如果之前用過 Bugly 的,并且閱讀 ReactNative 和 原生通信 這部分文檔的話,差不多十分鐘就可以內建完畢。
還不了解 ReactNative 和原生通信内容的,建議先閱讀一下本文檔了解一下。
Bugly 的注冊沒有什麼門檻,這裡直接使用個人 QQ 号就可以登入,建立一個專門為 ReactNative 測試的 App,然後根據文檔綁定對應的 AppID 即可。
不清楚的可以查閱 Bugly 的文檔:
https://bugly.qq.com/docs/user-guide/instruction-manual-android/?v=20171030170001
這部分内容沒什麼好說的,都是标準話的流程。接下來我們來看看內建它将面臨的坑。
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項目源碼。
推薦閱讀:
- 手寫你的第一個 Dalvik 版的 HelloWorld !
- 學程式設計,先學如何像程式員一樣思考!!!
- 看完九篇字型系列的文章,你還覺得我是在說字型?
- 通過 PackageManager 獲得你想要的 App 資訊!
- 阿裡全球首發的規約插件,所有細節都在這裡!!!
作者:承香墨影
出處:http://plokmju.cnblogs.com/
如非授權,禁止轉載!