阅读本文大概需要 5 分钟。
相对于其他类型的性能指标,卡顿是能直接让用户产生视觉反馈的现象,比如App反应滞后于用户的操作,在严重的情况下会出现ANR。关乎用户体验的大事,是很容易遭到用户吐槽的。因此,开发人员平时写代码时必须要时刻提醒自己不要落入卡顿的陷阱之中。
一. 卡顿原因
在罗列卡顿可能会发生的几个点之前,先简单介绍一下发生卡顿的原因。
在之前《handler系列》聊过,UI线程是基于queue中的message事件驱动的,事件 -> 执行 -> 下一个事件...,另一方面由于Android的帧率是60fps,也就是每16ms就会触发一次UI刷新,如果某个message的处理时间 > 16ms,就会导致接收到VSYNC信号的时候无法完成本次刷新操作,产生掉帧现象。
因此,从本质上来讲,我们必须让UI线程的任何事件在16ms之内解决战斗。
基于此,可能会导致卡顿的原因有三大类:
1)事件本身太耗时。
2)事件本身并不耗时,但需要等待别的地方返回耗时。
3)UI线程本身已经拿不到CPU资源来执行事件。
下面根据这三大类来分别具体细聊。
二. 耗时事件
这个很容易理解,就是把一些耗时业务逻辑直接写在了UI线程中,比如计算密集型的复杂计算,庞大的MD5计算,非对称RSA解密等。一般情况下,开发人员都不会犯这种错误,因为能够直接意识到计算量很大,本身就有警醒的作用。
三.耗时等待
1)网络I/O 同步请求
这种如果是在用以前比较老的网络库,比如URLConnection这种就需要开发人员自己来开启新的线程。开发者可能忘记开启子线程,又同时做了同步请求等待,导致卡顿的发生。但是现代网络库比如Okhttp,Retrofit已经帮我们准备好了线程池,一般不会再遇到。
2)磁盘I/O 文件,数据库
一般的文件和数据库操作,大家可能都会自觉的在子线程中操作。但是值得一提的是SharedPreference的存储和读取,根据sp的设计,创建的时候会开启子线程把整个文件全部加载进内存,加载完毕再通知主线程,如果读取尚未结束,此时想获取某个key的值,主线程就必须等待加载完毕为止。
因此,如果你的sp文件比较大,那么会带来几个严重问题:
a)第一次从sp中获取值的时候,有可能阻塞主线程,使界面卡顿、掉帧。
b)解析sp的时候会产生大量的临时对象,导致频繁GC,引起界面卡顿。
c)这些key和value会永远存在于内存之中,不会被释放,占用大量内存。
所以千万不要把庞大的key/value存在sp中,比如把复杂的json当value。
另外对于sp的存储,commit是同步操作,要在子线程中使用。而apply虽然是在子线程执行的,但是无节制地apply也会造成卡顿,原因是每次有系统消息发生的时候(handleStopActivity,handlePauseActivity)都会去检查已经提交的apply写操作是否完成,如果没有完成则阻塞主线程。
3)跨进程Binder同步等待返回数据
四.CPU时间片
1)其他应用发生抢占CPU资源的情况,导致本应用无法获得CPU执行时间片。
2)线程间发生死锁,UI线程无法获取锁,导致无法继续执行。
3)频繁GC,内存抖动。GC的次数越多,消耗在GC上的时间就越长,CPU花在界面绘制上的时间相应就越短。
五. 分析
对于卡顿的分析手段,有很多工具可以使用,下面介绍几种。
1)TraceView
相比之下,TraceView是分析卡顿的神兵利器,它不仅能看出每个方法消耗的时间、发生次数,并且可以进行排序,直接从最耗时的方法开始处优化。
2)ANR-WatchDog
其原理简单来说就是开启一个子线程,设置tick = interval,然后每隔一个interval(可设置)就往UI线程queue中扔一个runnable,若UI线程没卡顿,则interval时间内会取出此runnable执行,即重置tick,那么下一个interval循环时根据检测此tick是否被重置来判断是否有卡顿发生。如果有,则打印此时的各个线程运行时的stack trace(可设置只打印主线程),以帮助定位。
3)AndroidPerformanceMonitor
AndroidPerformanceMonitor 是国人开发的一个检测卡顿的开源库,原名是BlockCanary,可以设置卡顿检测时间,debug模式下检测到的卡顿可以通知展示(基本和LeakCanary一样),这个在开发自测时很有用。
其基本原理稍有不同,它并没有采用新开线程自己往UI线程里扔runnable的这种普通思想。而是利用系统在loop()方法里取出message前后进行了log打印这一特点,来重写Printer的println(String)方法,根据message处理前后的时间差,来判断是否发生了卡顿。
public static void loop() {
...
for (;;) {
...
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
...
}
}
而且这个工具在卡顿发生时,收集的信息还比较丰富,包括基本信息,耗时信息,CPU信息,堆栈信息等。
4)ANR trace.txt
而对于ANR,每当测试跑monkey一晚下来,ANR必是log的重点关注对象,若存在ANR,测试肯定会开jira贴上log给开发解决。对于trace.txt的分析,有几个基本的点是需要重点关注的:
a)具体的call stack指向的具体代码,是否是卡顿发生的原因。
b)是否有lock相关的关键字,代表可能发生死锁。
c)是否有iowait字样,是否在UI线程发生了网络或者磁盘I/O。
d)CPU使用率是否很高,很高表示要么自身有计算密集型任务发生,要么在其他地方有抢占CPU资源的任务。很低说明非耗时计算导致,可怀疑死锁和I/O耗时等待。
六. 解决
只要通过log分析能够找到发生卡顿的代码,基本上可以宣告问题很容易解决了,因为无论是对于耗时事件还是耗时等待,都可以采取异步的方式搞定。
而对于被抢占时间片的场景:
1)如果是死锁,则需要fix发生死锁的漏洞;
2)如果排除了以上所有可能后,就可以怀疑卡顿是由于被其他应用抢占CPU或者GC抖动导致,这需要通过log中的CPU使用率,和memory相关的回收信息,或者通过在debug模式下场景复现,综合profiler来观察和确定。
对于无法找到定位但是能够复现的场景,还可以根据业务场景来log打印时间,逐步缩小可疑代码的范围,从而排查和定位原因。
七. 总结
总之,关于卡顿的分析,并不是所有卡顿发生了都能找到原因,相反,大量ANR发生后通过log分析来解决是非常棘手的,甚至根本无从下手。所以我的观点是,对于卡顿一定要在开发写代码时做好警惕,养成良好习惯才是正道,防范为主,解决问题为辅。
进入公众号,回复“程序员“可以领取一份计算机技术电子书福利合集
欢迎转发,关注公众号 肖晖
每天几分钟,掌握一个硬核面试知识点