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