在 ReactNative 的 App 中,集成 Bugly 你会遇到的一些坑

一、前言

最近开新项目,准备尝试一下 ReactNative,所以前期做了一些调研工作,ReactNative 的优点非常的明显,可以做到跨平台,除了少部分 UI 效果可能需要对不同的平台进行单独适配,其中的核心逻辑代码,都是可以重用的。所以如果最终用 ReactNative 的话,可以省出某一端的客户端开发人员。而我这里调研的主要方向,就是它对国内第三方 SDK 的支持。

在国内,开发 App,一般都是会集成一些第三方服务的,例如:升级、崩溃分析、数据统计等等。而这些第三方服务,提供的 SDK ,通常只有 Native 层的,例如 Android 就是使用 Java 写的。而 ReactNative 本身 JavaScript 和 Native 层(Java层)的通信,其实已经做的很好了,所以大部分情况下,我们只需要对这些 SDK 做一个简单的封装就可以正常使用它了。

本期就来分享一下,如何在 ReactNative 的基础之上,集成 Bugly。这里主要是看它的崩溃搜集,这也是 Bugly 的主要功能。对于崩溃的收集,我主要关心两个部分:

  1. 是需要统计到正确的崩溃栈。
  2. 统计到的崩溃栈要是易于阅读的。

其实主要工作卡在了后者,接下来让我们具体看看问题。

本文的分析都是基于最新的 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() 方法中,有兴趣可以自行看看。

react-server

可以很清晰的看到,在 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 输出。

js-crash-stack

很尴尬的是,虽然崩溃栈也被输出出来了,和前面红屏的截图对比一下,也能发现它们其实是一个内容。但是,这些代码被混淆过了,如果 Native App 一样,混淆过的代码,反编译来看会变成 a.b.c ,这里的效果也是类似的。

这样的崩溃栈,其实拿出来,可读性非常的差,但是并不是不可读的。

那么接下来来看看如何定位到这个崩溃的真实代码,value@304:1133 这里,就是线索。我们把 Apk 解压,拿到其内 assets/index.android.bundle 文件,它其内就是我们 ReactNative 编译好的 Js 文件,可以看到它的第 304 行 1133 列,就是我们需要定位的出了问题的代码。

index-bundle

这样的编译后的代码,查 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,崩溃分析的一个完整的链路逻辑,我们只需要自己写个脚本工具,就可以帮我们精准定位了。

前面有点长,这里总结一下本小结的内容。

  1. ReactNative 不同的编译模式,使用的 JS 来源不同。Debug 模式来自 Packager Server,而 Release 模式,来自 Apk 的 assets 目录。
  2. Debug 模式下的崩溃,会触发红屏,而 Release 模式下的崩溃,会直接导致 App 崩溃。
  3. Debug 模式,之所以可以显示崩溃栈的基本信息,是因为编译的 JS 文件中,包含了对应的源文件和代码行号。而这些在 Release 模式下的 JS 是没有的。
  4. Release 模式的崩溃栈是被混淆后的,可以通过崩溃栈显示的行号和列号,来定位代码,但是无法定位具体源文件。
  5. 通过 react-native 命名,增加 --sourcemap-output参数,指定输出需要的混淆 Mapping 文件,它其内包含了混淆的信息。
  6. 解读 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-crash

3.3 解读 Bugly 中,js层的崩溃

Native 层的崩溃,和常规 App 一样,没什么好说的。这里只看 Js 层的崩溃信息。

js-stack

从这个崩溃栈你可以发现,其实下面 Java 的栈,基本上没有任何信息。这里主要是阅读上面 TypeError 后面的信息。这里描述了 Js 层崩溃的所有信息,包含错误和崩溃栈。

前面的内容如果认真看了,应该不难发现此处就是对 JS 崩溃输出的格式化拉平成一行了,所以如果我们要针对 Bugly 的崩溃栈编写解析脚本,就需要考虑到这些情况。

四、总结

本文说是 ReactNative 集成 Bugly 的一些坑,实际上讲的更多的是在生产环境下,如何分析 ReactNative 的崩溃栈。这些被搜集的原始信息,如何被还原成我们需要的信息。

不过这些,还是期待国内环境下,更多第三方 SDK 能支持到 ReactNative,毕竟官方团队支持的肯定要比我们自己写补丁脚本来的方便实用。

今天在承香墨影公众号的后台,回复『成长』。我会送你一些我整理的学习资料,包含:Android反编译、算法、Linux、虚拟机、设计模式、Web项目源码。

推荐阅读:

posted @ 2017-11-13 13:16  承香墨影  阅读(729)  评论(0编辑  收藏  举报