在ReactNative的App中,集成Bugly你一定會(huì)遇到的一些坑
一、前言
最近開新項(xiàng)目,準(zhǔn)備嘗試一下 ReactNative,所以前期做了一些調(diào)研工作,ReactNative 的優(yōu)點(diǎn)非常的明顯,可以做到跨平臺(tái),除了少部分 UI 效果可能需要對不同的平臺(tái)進(jìn)行單獨(dú)適配,其中的核心邏輯代碼,都是可以重用的。所以如果最終用 ReactNative 的話,可以省出某一端的客戶端開發(fā)人員。而我這里調(diào)研的主要方向,就是它對國內(nèi)第三方 SDK 的支持。
在國內(nèi),開發(fā) App,一般都是會(huì)集成一些第三方服務(wù)的,例如:升級(jí)、崩潰分析、數(shù)據(jù)統(tǒng)計(jì)等等。而這些第三方服務(wù),提供的 SDK ,通常只有 Native 層的,例如 Android 就是使用 Java 寫的。而 ReactNative 本身 JavaScript 和 Native 層(Java層)的通信,其實(shí)已經(jīng)做的很好了,所以大部分情況下,我們只需要對這些 SDK 做一個(gè)簡單的封裝就可以正常使用它了。
本期就來分享一下,如何在 ReactNative 的基礎(chǔ)之上,集成 Bugly。這里主要是看它的崩潰搜集,這也是 Bugly 的主要功能。對于崩潰的收集,我主要關(guān)心兩個(gè)部分:
- 是需要統(tǒng)計(jì)到正確的崩潰棧。
- 統(tǒng)計(jì)到的崩潰棧要是易于閱讀的。
其實(shí)主要工作卡在了后者,接下來讓我們具體看看問題。
本文的分析都是基于***的 ReactNative (v0.49) 版本來分析。
二、ReactNative 的崩潰統(tǒng)計(jì)
首先,ReactNative 中 JavaScript 和 Native 層的通信,官方文檔已經(jīng)寫的非常清楚了。在官方文檔中,舉了一個(gè) Toast 模塊的例子,寫的很清晰,這里就不再贅述了,還不了解的,可以先看看文檔。
ReactNative 原生模塊(中文文檔):
http://reactnative.cn/docs/0.49/native-modules-android.html#content
2.1 ReactNative 的編譯模式
而在 ReactNative 的程序中,實(shí)際上運(yùn)行的是 Js 的代碼,而它也是分 Debug 和 Release 的。
在 Debug 模式下,會(huì)從本地開啟一個(gè) Packager 服務(wù),然后 App 運(yùn)行起來之后,直接從服務(wù)里拉取***的編譯后的 JS 代碼,這樣可以在開發(fā)階段,做到代碼實(shí)時(shí)更新的效果,只需要在設(shè)備上,重新 Load 一下即可。
而在 Release 模式下,ReactNative 會(huì)將 JS 代碼,整體打包,然后放到 assets 目錄下,然后從這里去加載 JS 代碼。
這樣的邏輯被封裝在 ReactInstanceManager 類的 recreateReactContextInBackgroundInner() 方法中,有興趣可以自行看看。
可以很清晰的看到,在 Debug 和 Release ,分別使用的不同的方式,加載 JS 文件的。這里為什么要說到 ReactNative App 的編譯模式呢?其實(shí)和后面的邏輯有關(guān)系。
ReactNative 在 Debug 的情況下,其實(shí)還是很貼心的,如果出現(xiàn)崩潰的 Bug,會(huì)直接出紅屏,提示你崩潰的棧的具體信息,這些內(nèi)容可以幫助你快速的定位問題。
這里給的例子,是一個(gè) Js 層的崩潰,可以看到它崩潰棧中,很清晰的看到 App.js 文件的第 48 行 21列,會(huì)有一個(gè) ReferenceError 的錯(cuò)誤。
最方便的是,你直接點(diǎn)擊崩潰棧的代碼,會(huì)自動(dòng)打開對應(yīng)的 Js 文件。當(dāng)然,如果是一個(gè) Native 層的崩潰,雖然也會(huì)出紅屏,但是點(diǎn)擊并不能跳轉(zhuǎn)。
而假如現(xiàn)在同樣的代碼,使用 Release 模式的話,則會(huì)直接崩潰了。
2.2 不同編譯模式的 Js 有什么不同
假如 Release 和 Debug 一樣,可以有如此清晰的崩潰棧,其實(shí)問題就已經(jīng)得到解決。但是當(dāng)你使用 Release 包來觸發(fā)一個(gè)崩潰的時(shí)候,你就會(huì)發(fā)現(xiàn),它并不是一樣的。
使用命令,可以直接安裝一個(gè) Release 版本到設(shè)備上。
- cd android && ./gradlew installRelease
這里其實(shí)是兩行命令,先進(jìn)入到 android 項(xiàng)目的目錄,然后運(yùn)行 ./gradlew installRelease 這個(gè)沒什么好說的,如果運(yùn)行失敗,注意一下當(dāng)前 shell 環(huán)境的目錄路徑。
此時(shí),我們再運(yùn)行它就會(huì)直接導(dǎo)致崩潰,來看看崩潰的 Log 輸出。
很尷尬的是,雖然崩潰棧也被輸出出來了,和前面紅屏的截圖對比一下,也能發(fā)現(xiàn)它們其實(shí)是一個(gè)內(nèi)容。但是,這些代碼被混淆過了,如果 Native App 一樣,混淆過的代碼,反編譯來看會(huì)變成 a.b.c ,這里的效果也是類似的。
這樣的崩潰棧,其實(shí)拿出來,可讀性非常的差,但是并不是不可讀的。
那么接下來來看看如何定位到這個(gè)崩潰的真實(shí)代碼,value@304:1133 這里,就是線索。我們把 Apk 解壓,拿到其內(nèi) assets/index.android.bundle 文件,它其內(nèi)就是我們 ReactNative 編譯好的 Js 文件,可以看到它的第 304 行 1133 列,就是我們需要定位的出了問題的代碼。
這樣的編譯后的代碼,查 Bug 查起來就非常的費(fèi)時(shí)了,你首先需要根據(jù)當(dāng)前版本發(fā)布出去的 Apk,然后根據(jù)其中的 index.android.bundle 文件,定位到具體的代碼,之后再結(jié)合上下文全文搜索你的源代碼,才能找到對應(yīng)出錯(cuò)的代碼。
注意我這里本身項(xiàng)目就是一個(gè) Demo 項(xiàng)目,代碼量比較少,還能準(zhǔn)確的定位到問題,如果是一個(gè)實(shí)際的項(xiàng)目,在打 Release 包的時(shí)候,會(huì)將所有的 JS 文件全部打包到 index.android.bundle 文件中去。在這個(gè)例子中,如果 props.username.name 這段代碼,我在很多地方都用到的話,篩選它也是非常麻煩的。
2.3 Release 缺少了什么?
從前面的內(nèi)容可以了解到,Release 包同樣也是可以定位到出錯(cuò)的代碼的。但是,你依然需要全文的搜索這段代碼,無法精準(zhǔn)定位到具體出錯(cuò)代碼所在的源文件,這是為什么?
Release 包的 Js 一定是經(jīng)過混淆的,會(huì)剝離掉一些必要的信息,這些被剝離的信息,導(dǎo)致我們無法精準(zhǔn)定位到代碼的源文件上。
在 Debug 模式下,運(yùn)行我們的 Packager Server ,然后在瀏覽器中訪問:
http://localhost:8081/index.android.bundle?platform=android&dev=true
請確保你的 Packager Server 保持運(yùn)行的情況下訪問。
就可以看到當(dāng)前 Debug 模式,App 所運(yùn)行的 JS 代碼。我們直接根據(jù)出錯(cuò)代碼,精準(zhǔn)定位一下。
在這里,就可以很清晰的看到,它有一個(gè) fileName 和 lineNumber 兩個(gè)屬性,分別用來記錄當(dāng)前源碼的文件和這段代碼所在的行數(shù)。而回憶一下之前 Release 版本的 JS 代碼,你會(huì)發(fā)現(xiàn)關(guān)于源文件和行號(hào)的信息,被剝離了。
這也就是我們無法精準(zhǔn)定位出錯(cuò)代碼和鎖在源文件的根本原因。
2.4 Mapping
既然已經(jīng)明確的知道,在 Release 下,會(huì)過濾掉一些關(guān)于源文件和行號(hào)的信息,就如同 Android 的混淆一樣,那它是否包含類似對照關(guān)系的 Mapping 文件,可以幫助我們還原回去?
那么我們就需要找到 index.android.bundle 這個(gè)文件,是如何產(chǎn)生的。
ReactNative App 的打包,完全借助了 react.gradle 這個(gè)文件,你可以在 Android 工程的 build.gradle 文件中找到它。
繼續(xù)最終 node/modules 下的 react.gradle 文件。
可以看到它實(shí)際上是通過 react-native bundle 命令,通過增加參數(shù)的形式,輸出 index.android.bundle 文件的。
而如果你查閱文檔,你會(huì)發(fā)現(xiàn) react-native 命令,還有一個(gè)可配置的參數(shù) —sourcemap-output,它就是我們需要的。
完整的說明,你可以在這個(gè)網(wǎng)站上找到資料:
https://docs.bugsnag.com/platforms/react-native/showing-full-stacktraces/
我這里把關(guān)鍵信息截圖出來看著更清晰。
--sourcemap-output 命令非常的簡單,只需要配置一個(gè)輸出的文件名就可以了。
這里我們直接在命令行里運(yùn)行如下代碼,就可以自動(dòng)重新生成一個(gè) index.android.bundle 文件,并且同時(shí)也會(huì)生產(chǎn)一個(gè)對應(yīng)關(guān)系的 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
運(yùn)行效果如下:
注意這段命令,需要在 ReactNative 目錄的根目錄下執(zhí)行,否者會(huì)提示你找不到 node_module 。執(zhí)行完成,就可以在 ReactNative 項(xiàng)目目錄下,看到輸出的 android-release.bundle.map 文件了。
點(diǎn)開看看,完全看不懂,隨便截個(gè)圖讓大家感受一下。
其實(shí)到這里,已經(jīng)離我們的答案,更近一步了,Android 混淆的 Mapping 文件,也不是我們?nèi)庋勰芮逦炊?,我們接下來只需要找到它的解析?guī)則就可以了。
解析這個(gè) source-map ,NodeJs 為我們提供了一個(gè)專門的庫來解析,這里不多解釋,直接上代碼。
- /**
- * 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 列,我們運(yùn)行一下,看看輸出。
這段代碼,會(huì)很清晰的輸出對應(yīng)的源文件名和行號(hào),以及錯(cuò)的字段,還是很清晰的。
再來對照我們的源代碼驗(yàn)證一下。
確實(shí)也如 map.js 腳本輸出的一樣。
2.5 小結(jié)
到此,我們算是完成了 ReactNative App,崩潰分析的一個(gè)完整的鏈路邏輯,我們只需要自己寫個(gè)腳本工具,就可以幫我們精準(zhǔn)定位了。
前面有點(diǎn)長,這里總結(jié)一下本小結(jié)的內(nèi)容。
- ReactNative 不同的編譯模式,使用的 JS 來源不同。Debug 模式來自 Packager Server,而 Release 模式,來自 Apk 的 assets 目錄。
- Debug 模式下的崩潰,會(huì)觸發(fā)紅屏,而 Release 模式下的崩潰,會(huì)直接導(dǎo)致 App 崩潰。
- Debug 模式,之所以可以顯示崩潰棧的基本信息,是因?yàn)榫幾g的 JS 文件中,包含了對應(yīng)的源文件和代碼行號(hào)。而這些在 Release 模式下的 JS 是沒有的。
- Release 模式的崩潰棧是被混淆后的,可以通過崩潰棧顯示的行號(hào)和列號(hào),來定位代碼,但是無法定位具體源文件。
- 通過 react-native 命名,增加 --sourcemap-output參數(shù),指定輸出需要的混淆 Mapping 文件,它其內(nèi)包含了混淆的信息。
- 解讀 ReactNative Mapping 文件,可以使用 source-map 這個(gè) NodeJs 庫來進(jìn)行解析,可以精準(zhǔn)定位到行號(hào)和源文件名。
三、集成 Bugly 的坑
Bugly 的集成,非常的簡單。如果之前用過 Bugly 的,并且閱讀 ReactNative 和 原生通信 這部分文檔的話,差不多十分鐘就可以集成完畢。
還不了解 ReactNative 和原生通信內(nèi)容的,建議先閱讀一下本文檔了解一下。
ReactNative 原生模塊(中文文檔):
http://reactnative.cn/docs/0.49/native-modules-android.html#content
Bugly 的注冊沒有什么門檻,這里直接使用個(gè)人 QQ 號(hào)就可以登錄,創(chuàng)建一個(gè)專門為 ReactNative 測試的 App,然后根據(jù)文檔綁定對應(yīng)的 AppID 即可。
不清楚的可以查閱 Bugly 的文檔:
https://bugly.qq.com/docs/user-guide/instruction-manual-android/?v=20171030170001
這部分內(nèi)容沒什么好說的,都是標(biāo)準(zhǔn)話的流程。接下來我們來看看集成它將面臨的坑。
3.1 Debug 模式下不會(huì)上報(bào)崩潰
之前也提到,Debug 模式下,如果觸發(fā)了崩潰,會(huì)直接進(jìn)入紅屏狀態(tài),顯示當(dāng)前崩潰棧的信息。這個(gè)功能,在我們開發(fā)階段,非常的好用,能快速定位問題。
但是正是因?yàn)?ReactNative 會(huì)在 Debug 模式下,Hook 住我們的崩潰棧,從而會(huì)導(dǎo)致 Bugly SDK 無法搜集到對應(yīng)的崩潰也就無法進(jìn)行上報(bào)。
所以,如果你在 ReactNative 項(xiàng)目內(nèi),集成了 Bugly 之后。造的崩潰沒有得到上報(bào),檢查一下自己編譯模式,一定要切換到 Release 模式下。
3.2 崩潰信息整合
Bugly 為了方便開發(fā)者查看,會(huì)將類似崩潰棧的崩潰,整合成一個(gè),然后進(jìn)行計(jì)數(shù)統(tǒng)計(jì),只顯示當(dāng)前崩潰了多少次和影響的人數(shù)。
而在 ReactNative 項(xiàng)目中,如果是 Native 層出現(xiàn)的崩潰,那其實(shí)沒有什么差別,崩潰信息和我們平時(shí)開發(fā)常規(guī) App 一樣。
但是,如果這個(gè)崩潰是發(fā)生在 Js 層的話,它最終會(huì)把崩潰拋到 Native 層,同樣也是可以統(tǒng)計(jì)的的。但是這些崩潰會(huì)被封裝成一個(gè) JavascriptException 拋出來,從而導(dǎo)致它們被簡單的歸為了 JavascriptException ??赡芩鼈兠枋龅氖遣煌? Bug,但是卻被歸位一類,這樣之后查閱起來,就需要人工進(jìn)行篩選。
這里看兩個(gè)崩潰,***個(gè)發(fā)生在 Js 層,第二個(gè)發(fā)生在 Native 層。
3.3 解讀 Bugly 中,js層的崩潰
Native 層的崩潰,和常規(guī) App 一樣,沒什么好說的。這里只看 Js 層的崩潰信息。
從這個(gè)崩潰棧你可以發(fā)現(xiàn),其實(shí)下面 Java 的棧,基本上沒有任何信息。這里主要是閱讀上面 TypeError 后面的信息。這里描述了 Js 層崩潰的所有信息,包含錯(cuò)誤和崩潰棧。
前面的內(nèi)容如果認(rèn)真看了,應(yīng)該不難發(fā)現(xiàn)此處就是對 JS 崩潰輸出的格式化拉平成一行了,所以如果我們要針對 Bugly 的崩潰棧編寫解析腳本,就需要考慮到這些情況。
四、總結(jié)
本文說是 ReactNative 集成 Bugly 的一些坑,實(shí)際上講的更多的是在生產(chǎn)環(huán)境下,如何分析 ReactNative 的崩潰棧。這些被搜集的原始信息,如何被還原成我們需要的信息。
不過這些,還是期待國內(nèi)環(huán)境下,更多第三方 SDK 能支持到 ReactNative,畢竟官方團(tuán)隊(duì)支持的肯定要比我們自己寫補(bǔ)丁腳本來的方便實(shí)用。
今天在承香墨影公眾號(hào)的后臺(tái),回復(fù)『成長』。我會(huì)送你一些我整理的學(xué)習(xí)資料,包含:Android反編譯、算法、設(shè)計(jì)模式、虛擬機(jī)、Linux、Web項(xiàng)目源碼。
【本文為51CTO專欄作者“張旸”的原創(chuàng)稿件,轉(zhuǎn)載請通過微信公眾號(hào)聯(lián)系作者獲取授權(quán)】