@avenwu
2017-09-07T06:04:00.000000Z
字数 5723
阅读 1170
在分析崩溃bug的时候,我们非常倚重调用栈日志,他在大多数情况下可以帮助开发者定位崩溃的逻辑链路,比如是哪个页面的控件出现了什么空指针之类的。
但是总有那么些个崩溃是没有这种有效日志的,没有办法定位崩溃源的样子真让人感到无助。
# main(1)java.lang.NullPointerExceptionAttempt to invoke virtual method 'int android.view.View.getVisibility()' on a null object reference1 android.widget.FrameLayout.layoutChildren(FrameLayout.java:531)2 android.widget.FrameLayout.onLayout(FrameLayout.java:514)3 android.view.View.layout(View.java:15695)4 android.view.ViewGroup.layout(ViewGroup.java:5048)//...23 android.os.Handler.dispatchMessage(Handler.java:95)24 android.os.Looper.loop(Looper.java:147)25 android.app.ActivityThread.main(ActivityThread.java:5513)26 java.lang.reflect.Method.invoke(Native Method)27 java.lang.reflect.Method.invoke(Method.java:372)28 com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:969)29 com.android.internal.os.ZygoteInit.main(ZygoteInit.java:764)
诸如此类信息不足以定位的bug还是很常见的。
为了方便定位问题,我们在早些时候时候写了一个Tango小模块,专门用来收集崩溃信息。
彼时我们根据需要定义了一套收集的内容和传输协议。
{"process": "process name","taskInfo": {"topActivity": "component name","baseActivity": "component name","origActivity": "optional component name"},"dump": {"activity": "activity name","pluginName": "optional package name","pluginClassName": "optional real activity name","fragment#0": "fragment data","deep": "optional hierarchy dump deep","hierarchy": "optional view stack"}}

经过一段时间的使用,这个模块有效的帮助我们定位解决了大量bug,现在几乎成了分析bug的一个必经流程,比如通过他定位bug的模块关系,从而分配负责修复的RD。
由于业务的特殊性,在我们58App当中,存在大量的业务线,也存在很多载体相同但是动态承载分类业务的情况,比如我们的黄页大类下面有成百个自己分类,每个分类都可以进入对应的分类列表页面。
这个时候如果这种共性在载体页面发生了崩溃,我们如何定位他的业务类别呢?
基于此,我们想到扩展协议,让Tango手机必要的参数用于是被页面信息。通过梳理业务逻辑和思考,我们知道在Andorid中页面载体主要是Activity和Fragment,他们之间的跳转和承载必然会涉及到页面间的参数,而我们的业务线,实际上也是通过常规的intent和bundle进行数据传递的。因此我们的扩展非常明确,支持收集Activity的bundle[1]信息和Fragment的Bundle[2]信息。

{"process": "process name","taskInfo": {"topActivity": "component name","baseActivity": "component name","origActivity": "optional component name"},"dump": {"activity": "activity name","extras": "Bundle[]","fragments": [{"bundle": "Bundle[]","fragment": "fragment name"}],"pluginName": "optional package name","pluginClassName": "optional real activity name","deep": "optional hierarchy dump deep","hierarchy": "optional view stack"}}
在收集的时候,需要注意兼容一下android.support.v4.app.Fragment 和 android.app.Fragment
过去我们分析上传的数据主要是人工,原因也很简单,为了让数据和bug能够一一吻合上,我们是直接把数据和崩溃信息一起上传到bugly[3]这边的。但是目前bugly并没有提供相关API供我们做数据分析和二次开发。所以我们的分析就是在bugly查询页面直接查看对应的日志,如果数据较多就复制出来通过json工具进行格式化后再阅读。

这个流程基本上是可行的,但是也有他的缺点:
阅读日志的RD必须理解我们的的协议格式,起码要知道关键字段的含义。
现在我们的协议字段内容已经比较多了,如果继续按以前的老版本手工提取日志然后格式化查看的话就显得比较费劲了。因此我们设计并开发了专门用于提取Tango协议日志的工具 Tango助手[4]。
Tango助手基于Chrome Extension[5] 技术开发, 可以在我们查阅bug详情时,自动提取并解析上传到bugly上的定制信息。
他的好处在于:

我们的数据面板有两块,通过设置项可以设置插件自动识别完日志后谭树提示框,这个信息只做了简单提取Activity,和格式化;
如果觉得自动弹窗比较烦人那么可以设置插件不弹窗(默认是不弹窗),这个时候日志提取完了,上哪去看呢?可以点击右上角的插件按钮,会弹出相对详细的内容。
如果说由于web页面大改版了或者bug什么的,导致插件没有自动提取到数据时怎么办呢,还可以手动提取,选中日志右键菜单选择提取日志就可以了,这个时候会再次识别选中内容并弹出提示面板。
好了,是时候讲一下我们的插件是如何工作的。

跟踪日志/自定义日志可以看到我们上传的日志,到这整个流程都是一样的;基本上这就是我们的整个工作流程。如果懂js的话,基本可以照着整个思路直接实现了。
这里提几个extension开发需要注意的一些点:
所以我们的整体流程的工作其实是分散在不同js当中的,不同的面板触发也是不同逻辑。
讲了这么多,还不放点代码么?
控制插件按钮的可用性,如果没有命中日志,我们希望按钮点击不生效,只有有数据的时候才启用。这个实现实际上是通过api动态设置按钮的状态实现的。
function updateBrowserAction(enable) {if (enable) {chrome.browserAction.enable();chrome.browserAction.setTitle({title: i18n('browser_action_enable_tips')});chrome.browserAction.setBadgeText({text: "new"});chrome.browserAction.setBadgeBackgroundColor({color: '#f44336'});} else {chrome.browserAction.disable();chrome.browserAction.setTitle({title: i18n('browser_action_disable_tips')});}}
你是怎么过滤的DOM数据? 这个使功能使我们的插件真正做到了自动解析,那么简单看下代码吧;
function selectTangoLogElements() {console.log("select log elements...");// this elements sequence base on web page layout$('td span:nth-of-type(2)').each(function (index, element) {if (element.innerText.indexOf('Tango: ') !== -1) {var innerText = element.innerText;var logString = innerText.split('Tango: ')[1];console.log(logString);try {var tangoLog = JSON.parse(logString);tangoLog.timestamp = new Date().toUTCString();chrome.runtime.sendMessage({parseTangoLog: true,tangoLog: tangoLog}, function (r) {console.log('send message', r);});} catch (e) {console.log('skip invalid log', e);}}})}
实际上就是拦截了dom元素,然后根据我们提前观察,得出日志所在的dom树中的位置,然后就提取啦。
为什么你的弹框这么大,alert应该是很小很丑的以一个样子啊。这个确实比较麻烦, 笔者毕竟不是很懂前端的一套,想要我自定义一个alert是困难的。因此我的做法是往当前的dom里面插入一个div,这个div动态控制显示状态模拟出alert弹窗的效果就好了。
$('#modal1').modal({dismissible: true, // Modal can be dismissed by clicking outside of the modalopacity: .5, // Opacity of modal backgroundinDuration: 300, // Transition in durationoutDuration: 200, // Transition out durationstartingTop: '4%', // Starting top style attributeendingTop: '10%', // Ending top style attributeready: function (modal, trigger) { // Callback for Modal open. Modal and trigger parameters available.console.log(modal, trigger);},complete: function () {console.log('Closed');unloadCSS('materialize-fonts-icon');unloadCSS('materialize-min');} // Callback for Modal close});$('#modal1').modal('open');
示例就讲到这,最后我们在简单总结下。
一:PHP是最好的语言?
二:插件里面主要使用的js,如果有些语法写的不正确,请原谅,我只懂这么多啦。
三:对我们的异常收集有什么其他建设性的建议,欢迎指正。