最近开新项目,准备尝试一下 ReactNative,所以前期做了一些调研工作,ReactNative 的优点非常的明显,可以做到跨平台,除了少部分 UI 效果可能需要对不同的平台进行单独适配,其中的核心逻辑代码,都是可以重用的。所以如果最终用 ReactNative 的话,可以省出某一端的客户端开发人员。而我这里调研的主要方向,就是它对国内第三方 SDK 的支持。
在国内,开发 App,一般都是会集成一些第三方服务的,例如:升级、崩溃分析、数据统计等等。而这些第三方服务,提供的 SDK ,通常只有 Native 层的,例如 Android 就是使用 Java 写的。而 ReactNative 本身 JavaScript 和 Native 层(Java层)的通信,其实已经做的很好了,所以大部分情况下,我们只需要对这些 SDK 做一个简单的封装就可以正常使用它了。
本期就来分享一下,如何在 ReactNative 的基础之上,集成 Bugly。这里主要是看它的崩溃搜集,这也是 Bugly 的主要功能。对于崩溃的收集,我主要关心两个部分:
其实主要工作卡在了后者,接下来让我们具体看看问题。
本文的分析都是基于最新的 ReactNative (v0.49) 版本来分析。
首先,ReactNative 中 JavaScript 和 Native 层的通信,官方文档已经写的非常清楚了。在官方文档中,举了一个 Toast 模块的例子,写的很清晰,这里就不再赘述了,还不了解的,可以先看看文档。
ReactNative 原生模块(中文文档):
http://reactnative.cn/docs/0.49/native-modules-android.html#content
而在 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 模式的话,则会直接崩溃了。
假如 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
这段代码,我在很多地方都用到的话,筛选它也是非常麻烦的。
从前面的内容可以了解到,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 代码,你会发现关于源文件和行号的信息,被剥离了。
这也就是我们无法精准定位出错代码和锁在源文件的根本原因。
既然已经明确的知道,在 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
脚本输出的一样。
到此,我们算是完成了 ReactNative App,崩溃分析的一个完整的链路逻辑,我们只需要自己写个脚本工具,就可以帮我们精准定位了。
前面有点长,这里总结一下本小结的内容。
--sourcemap-output
参数,指定输出需要的混淆 Mapping 文件,它其内包含了混淆的信息。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
这部分内容没什么好说的,都是标准话的流程。接下来我们来看看集成它将面临的坑。
之前也提到,Debug 模式下,如果触发了崩溃,会直接进入红屏状态,显示当前崩溃栈的信息。这个功能,在我们开发阶段,非常的好用,能快速定位问题。
但是正是因为 ReactNative 会在 Debug 模式下,Hook 住我们的崩溃栈,从而会导致 Bugly SDK 无法搜集到对应的崩溃也就无法进行上报。
所以,如果你在 ReactNative 项目内,集成了 Bugly 之后。造的崩溃没有得到上报,检查一下自己编译模式,一定要切换到 Release 模式下。
Bugly 为了方便开发者查看,会将类似崩溃栈的崩溃,整合成一个,然后进行计数统计,只显示当前崩溃了多少次和影响的人数。
而在 ReactNative 项目中,如果是 Native 层出现的崩溃,那其实没有什么差别,崩溃信息和我们平时开发常规 App 一样。
但是,如果这个崩溃是发生在 Js 层的话,它最终会把崩溃抛到 Native 层,同样也是可以统计的的。但是这些崩溃会被封装成一个 JavascriptException 抛出来,从而导致它们被简单的归为了 JavascriptException 。可能它们描述的是不同的 Bug,但是却被归位一类,这样之后查阅起来,就需要人工进行筛选。
这里看两个崩溃,第一个发生在 Js 层,第二个发生在 Native 层。
Native 层的崩溃,和常规 App 一样,没什么好说的。这里只看 Js 层的崩溃信息。
从这个崩溃栈你可以发现,其实下面 Java 的栈,基本上没有任何信息。这里主要是阅读上面 TypeError 后面的信息。这里描述了 Js 层崩溃的所有信息,包含错误和崩溃栈。
前面的内容如果认真看了,应该不难发现此处就是对 JS 崩溃输出的格式化拉平成一行了,所以如果我们要针对 Bugly 的崩溃栈编写解析脚本,就需要考虑到这些情况。
本文说是 ReactNative 集成 Bugly 的一些坑,实际上讲的更多的是在生产环境下,如何分析 ReactNative 的崩溃栈。这些被搜集的原始信息,如何被还原成我们需要的信息。
不过这些,还是期待国内环境下,更多第三方 SDK 能支持到 ReactNative,毕竟官方团队支持的肯定要比我们自己写补丁脚本来的方便实用。
今天在承香墨影公众号的后台,回复『成长』。我会送你一些我整理的学习资料,包含:Android反编译、算法、Linux、虚拟机、设计模式、Web项目源码。
推荐阅读: