在 ReactNative 的 App 中,集成 Bugly 你
一、前言
最近开新项目,准备尝试一下 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-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 输出。
[图片上传失败...(image-c5408a-1510550027153)]
很尴尬的是,虽然崩溃栈也被输出出来了,和前面红屏的截图对比一下,也能发现它们其实是一个内容。但是,这些代码被混淆过了,如果 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 代码。我们直接根据出错代码,精准定位一下。
debug-server在这里,就可以很清晰的看到,它有一个 fileName 和 lineNumber 两个属性,分别用来记录当前源码的文件和这段代码所在的行数。而回忆一下之前 Release 版本的 JS 代码,你会发现关于源文件和行号的信息,被剥离了。
这也就是我们无法精准定位出错代码和锁在源文件的根本原因。
2.4 Mapping
既然已经明确的知道,在 Release 下,会过滤掉一些关于源文件和行号的信息,就如同 Android 的混淆一样,那它是否包含类似对照关系的 Mapping 文件,可以帮助我们还原回去?
那么我们就需要找到 index.android.bundle 这个文件,是如何产生的。
ReactNative App 的打包,完全借助了 react.gradle 这个文件,你可以在 Android 工程的 build.gradle 文件中找到它。
app-gradle继续最终 node/modules 下的 react.gradle 文件。
react-gradle可以看到它实际上是通过 react-native bundle
命令,通过增加参数的形式,输出 index.android.bundle 文件的。
而如果你查阅文档,你会发现 react-native
命令,还有一个可配置的参数 —sourcemap-output
,它就是我们需要的。
完整的说明,你可以在这个网站上找到资料:
https://docs.bugsnag.com/platforms/react-native/showing-full-stacktraces/
我这里把关键信息截图出来看着更清晰。
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注意这段命令,需要在 ReactNative 目录的根目录下执行,否者会提示你找不到 node_module 。执行完成,就可以在 ReactNative 项目目录下,看到输出的 android-release.bundle.map 文件了。
点开看看,完全看不懂,随便截个图让大家感受一下。
map其实到这里,已经离我们的答案,更近一步了,Android 混淆的 Mapping 文件,也不是我们肉眼能清晰看懂的,我们接下来只需要找到它的解析规则就可以了。
解析这个 source-map ,NodeJs 为我们提供了一个专门的库来解析,这里不多解释,直接上代码。
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这段代码,会很清晰的输出对应的源文件名和行号,以及错的字段,还是很清晰的。
再来对照我们的源代码验证一下。
error-line确实也如 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 原生模块(中文文档):
http://reactnative.cn/docs/0.49/native-modules-android.html#content
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 层。
bugly-crash3.3 解读 Bugly 中,js层的崩溃
Native 层的崩溃,和常规 App 一样,没什么好说的。这里只看 Js 层的崩溃信息。
js-stack从这个崩溃栈你可以发现,其实下面 Java 的栈,基本上没有任何信息。这里主要是阅读上面 TypeError 后面的信息。这里描述了 Js 层崩溃的所有信息,包含错误和崩溃栈。
前面的内容如果认真看了,应该不难发现此处就是对 JS 崩溃输出的格式化拉平成一行了,所以如果我们要针对 Bugly 的崩溃栈编写解析脚本,就需要考虑到这些情况。
四、总结
本文说是 ReactNative 集成 Bugly 的一些坑,实际上讲的更多的是在生产环境下,如何分析 ReactNative 的崩溃栈。这些被搜集的原始信息,如何被还原成我们需要的信息。
不过这些,还是期待国内环境下,更多第三方 SDK 能支持到 ReactNative,毕竟官方团队支持的肯定要比我们自己写补丁脚本来的方便实用。
今天在承香墨影公众号的后台,回复『成长』。我会送你一些我整理的学习资料,包含:Android反编译、算法、Linux、虚拟机、设计模式、Web项目源码。
推荐阅读:
image