【腾讯Bugly干货分享】经典随机Crash之二:Android消息机制

2017-04-18 10:21:25来源:oschina作者:腾讯Bugly人点击



>本文作者:鲁可——腾讯SNG专项测试组 测试工程师
![](/2014th7cj/d/file/p/20170418/2edrna1getx.png)
## 背景
承上经典随机Crash之一:线程安全
## 问题的模型
好几次灰度top1、top2 Crash发生场景:在很平常、频繁的使用页面,打开一个界面,马上返回,piaji,挂了,估计用户心中有千万只草泥马在奔腾,手机QQ究竟怎么呢?
找到开发童鞋,还是熟悉的对话:
1. 请教:这个Crash能复现吗?开发答:场景就在这,就是复现不了啊
2. 这里有个空指针,那我就加个判空我只好去看下开发童鞋的代码,发现都有一个共性,跟`handler postDelayed`有关系,这里抽取出Crash代码梗概
![](/2014th7cj/d/file/p/20170418/jo3ujchvwhs.jpg)
Post一个匿名`Runnable`,延迟500ms
![](/2014th7cj/d/file/p/20170418/e3j1rm43q3a.jpg)
跟开发童鞋反复再三确认,`mGLVideoView`置空的地方只有一处,就在`onDestroy()`中
![](/2014th7cj/d/file/p/20170418/ivb2uygcnue.jpg)
开发童鞋一般为了解决内存泄露问题,会在`onDestroy`中将变量置空,以让系统回收,这么做也理所当然。跟用户反馈的情况也吻合,打开界面,立马返回,会Crash。
为了搞清这个问题的根源,需要对Android消息机制有一定了解,大家可以搜索下相关文章。
![](/2014th7cj/d/file/p/20170418/vm5yde3rk5u.jpg)
不按套路出牌,碉堡了的用户是这样的,如图所示
![](/2014th7cj/d/file/p/20170418/nfyb3xofwt5.gif)
弱爆了的我是这样的,如图所示
![](/2014th7cj/d/file/p/20170418/i1ylz0itv41.gif)
那接下来的事情就好办了,寻找腾讯手速最快的人,要在500ms之内打开界面,返回,要是他都复现不了,那就真的复现不了,虽然是开个玩笑,但这确实已经不是个概率性问题了,在我们手速不够快的情况下,这类型Crash确实是复现不了,但很显然这不是解决问题的正确姿势。
## 解决问题的思路
事后手段:
1. 加判空
2. 这里给大家推荐这篇文章:
>Android handler.removeCallbacksAndMessages(null)的妙用
http://www.snowdream.tech/2016/02/18/handler-removeCallbacksAndMessages/**好处有**:非静态匿名内部类`Runnable`持有外部类会导致内存泄露,`remove`掉以较少内存泄露;消除这类空指针Crash的隐患;减少主线程消息队列的任务,还能提升点性能
然而这些都不能做到事前发现,今天我们就一起来探讨下一些事前的手段,并解密一个我申请的有利于发现同类问题的专利。
请教了做静态检查的同学,在没有任何上下文环境的情况下直接使用一个变量,这种空指针检查很难搞,我们主要从动态角度上分析。
#### 1、 在`activity onDestroy`之后`handler.post`
监控`Activity onDestroy` 、`handler post`操作,强制在`onDestroy`之后再`post`,就能100%复现这个Crash了
那首先需要寻找Activity与handler之间的联系,监控`onDestroy`,可以用hook或者类似LeakCanary的方式,注册`ActivityLifecycleCallbacks`来监听,但难点在怎么把`handler post`跟`Activity onDestroy`建立起联系,从开发者的角度来说,这两个模块没有联系,Activity完全不用handler也是可以的,在Activity的生命周期方法中,没有哪个需要带上handler,Activity中会不会默认隐藏着handler了?
抱着这样的疑问,我去看了下Activity的源码(以Android5.0为准)
果真Activity中会有一个`mHandler`
![](/2014th7cj/d/file/p/20170418/qnwk3c1ymt4.jpg)
看了下这个`mHandler`在什么地方会被用到
![](/2014th7cj/d/file/p/20170418/3wrnqovlgmr.jpg)
只有在`runOnUiThread`中会被用到,但开发者自己绑定`MainLooper`的`handler`跟这个`mHandler`没有关系。
这种方法需要对`Activity Handler`两大核心模块找到一种关联,并做一种高精度的手术,限于本人能力有限,一时陷入了困境。
#### 2、 控制消息的时机
既然没法找到`Activity Handler`的关联,就只好从消息机制本身着手。
刚开始我们想到的方法,把这种消息从消息队列里取出来,等待时机,然后再重新插入消息队列
那第一步就需要把这种消息取出来,我们先来看看源码是怎么做的
![](/2014th7cj/d/file/p/20170418/x3aox0d5lgk.jpg)
在`loop()`中会通过`next()`获取一个消息,如果能获取到,则通过`dispatchMessage()`分发消息,接下来我们看看next()是怎么获取消息的
![](/2014th7cj/d/file/p/20170418/z4zodbcuxew.jpg)
![](/2014th7cj/d/file/p/20170418/akjgbf5d5qv.jpg)
在`next`获取了当前系统时间,若到了消息执行时间,则返回消息
这里一定会有疑问,`msg.when`是怎么设置的?消息是如何插入队列的?
`next()`从消息队列获取一个消息,无法精准到具体的消息,其实我们还可以参考`removeMessages`的实现,通过反射来取出消息,如果`remove`的时机过晚,也会导致这个消息已经被消费了,如果`remove`错了,导致丢消息,篓子就捅大了。总之,我们必须搞清楚消息入队列的过程。
发送消息主要有sendXXX,postXXX两大类方法,由于Runnable也会被封装成Message
![](/2014th7cj/d/file/p/20170418/kyttzf0k1ja.jpg)
![](/2014th7cj/d/file/p/20170418/xahtky1ffg4.jpg)
其实这里面也会有个坑:Callback类型Message的what是0,大家有兴趣也可以学习下
看过`post (runnable)`、`sendMessage`过程后,我画了一个postXXX、sendXXX调用关系图
![](/2014th7cj/d/file/p/20170418/si02qn35xqu.jpg)
根据上面的图,可以看出`sendMessageDelayed`和`sendMessageAtTime`是非常重要的两个环节,我们来看下这两个方法究竟做了啥
![](/2014th7cj/d/file/p/20170418/whjhkrj1fom.jpg)
在`sendMessageDelayed`中会用系统开机总时间`+dalayMillis`,所以传入`sendMessageAtTime`的值是相对于系统启动的绝对值
![](/2014th7cj/d/file/p/20170418/555vfjitamx.jpg)
![](/2014th7cj/d/file/p/20170418/ewp4vgw1fwh.jpg)
再来看`queue.enqueueMessage`的过程
![](/2014th7cj/d/file/p/20170418/d4zkj2vb4b1.jpg)
`when`赋值给了`msg.when`,这下能解释`next()`中`msg.when`是如何得来的问题,到这里,您应该清楚了,原来插入消息队列的顺序是根据`msg.when`大小来插入的。
前面说到`when`传入的是一个绝对值,那上面为啥有`when==0`的判断,那什么时候`when`会为0呢?当把一个消息强制插入到队列首的位置,会传入0
![](/2014th7cj/d/file/p/20170418/nusmjwyfc25.jpg)
如果我们要延迟那个消息的处理时机,只需改动这个绝对值就可以了,我们决定通过`hook sendMessageDelayed`,将延迟时间`delayMillis`改长,如果您看到这里,是不是觉得方案其实很简单?确实是的,如果我一上来就告诉您这么做,那这个问题就很简单了,其实中间也是踩了一些坑,然而知道为什么要这么做,似乎更重要,也更有趣。
到此,您已经清楚Android是如何插入消息的了,您要是愿意,完全可以把全部消息hook住了,随意改`uptimeMillis`,那您已经掌握了玩弄消息顺序于股掌之中的技术。
## 问题的解决方案
最终综合安全性、稳定性等方面的考虑,我们采用了将`delayMillis`时间改长的方案
1. 考虑到主线程做了很多事情,比如需处理绘制UI等一些系统消息,而开发者一般把延时操作都放在了`Runnable`里,这里我们只延迟`Runnable`经过封装的消息,并根据调用堆栈做了过滤
2. 考虑到这种Crash容易发生在post短时间内,如果开发者本来设置的延迟时间就比较大,如果再加大延迟,会让消息得不到及时处理,所以我们对需要加大延迟的时间做了阈值判断最终实现的流程图如下图所示:
![](/2014th7cj/d/file/p/20170418/y5toaf1m1rh.jpg)
**因此,这个专利水到渠成:一种延迟消息分发模拟Crash的方法**
最终要达到的效果下图所示:
![](/2014th7cj/d/file/p/20170418/wn100d2uhkq.gif)
众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。延迟一个小时,我完全可以出去吃个饭、遛个弯,再回来复现这个Crash了。**问:跟当前主线程卡顿监控方案是否有冲突?**
>答:主线程卡顿监控主要是计算`dispatchMessage`,`Dispatching`、`Finished`之间的耗时,我们对`dispatchMessage`没做任何手脚,只是延迟了消息的处理时机。
**问:会不会造成卡顿?**
>答:UI上的不流畅主要是掉帧,每个消息具体耗时多少,还是取决于消息本身在做什么,我们跟开发者自己把`delayMillis`改长并没什么区别。
## 效果
延迟消息分发SDK已加入NewMonkey随身版挑战者模式中,能做到无场景延迟`Runnable`类型消息的分发,功能上线短短1天内,就发现了Android QQ 4个Crash,都得到了开发同学的迅速fix。
由于本人能力、精力有限,对Android消息机制远未啃透,若有纰漏,欢迎斧正,对其他平台的消息机制更是一窍不通,若对您有所启发,深感荣幸。
道高一尺魔高一丈,在降Crash率上,依旧任重而道远。
----------
更多精彩内容欢迎关注[腾讯 Bugly](https://bugly.qq.com/)的微信公众账号:
![](/2014th7cj/d/file/p/20170418/pokovkyduwd.png)
[腾讯 Bugly](https://bugly.qq.com/)是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 [Crash](https://bugly.qq.com/) 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用,快来加入我们吧!

最新文章

123

最新摄影

微信扫一扫

第七城市微信公众平台