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