今天来聊聊卡顿的问题,除了内存泄漏,页面的流畅度也非常重要。我目前使用的最流畅的 app 就是 Telegram 了。那么在 Android 中如何检测卡顿呢?
目录:
- 卡顿的原因
- BlockCannary
- StrictMode
1. 卡顿的原因
我总结了以下几点:
- 在主线程处理耗时任务,比如处理 IO,操作数据库,数据计算等。
- 在主线程进行网络请求,当然在 Android 4.0 后,在主线程进行网络请求会抛出异常。
- 解析 xml 布局或者 new 的视图层级过多。
- 调用系统硬件,比如相机。
- 加载图片没有处理好。
- 启动的线程过多,cpu 占用率过高。
那么如何来查找卡顿呢?有两种方式:
- 1. 使用工具 BlockCannary,带有图形界面,可以设置卡顿的阀值。
- 2. 使用 Android 自带的工具类 StrictMode。
2. BlockCannary
- 2.1 简介
BlockCanary 是国内开发者 MarkZhai 开发的一套性能监控组件,它对主线程操作进行了完全透明的监控,并能输出有效的信息,帮助开发分析、定位到问题所在,迅速优化应用。
其特点有:
- 非侵入式,简单的两行就打开监控,不需要到处打点,破坏代码优雅性。
- 精准,输出的信息可以帮助定位到问题所在 (精确到行),不需要像 Logcat 一样,慢慢去找。
目前包括了核心监控输出文件,以及 UI 显示卡顿信息功能。
使用文档:https://github.com/markzhai/AndroidPerformanceMonitor/blob/master/README_CN.md
- 2.2 原理
Android 中主线程 ActivityThread 创建一个Looper (Looper.prepare),而 Looper 又会关联一个 MessageQueue,主线程 Looper会在应用的生命周期内不断轮询 (Looper.loop),从 MessageQueue 取出 Message 更新 UI。
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);
}
...
}
}
msg.target 其实就是 Handler,看一下 dispatchMessage() 的逻辑:
/**
* Handle system messages here.
*/
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
- 如果消息是通过 Handler.post(runnable) 方式投递到 MQ 中的,那么就回调 runnable#run 方法;
- 如果消息是通过 Handler.sendMessage() 的方式投递到 MQ 中,那么回调 handleMessage 方法;
不管是哪种回调方式,回调一定发生在 UI 线程。因此如果应用发生卡顿,一定是在 dispatchMessage() 中执行了耗时操作。我们通过给主线程的 Looper 设置一个 Printer,打点统计 dispatchMessage() 方法执行的时间,如果超出阀值,表示发生卡顿,则dump 出各种信息,提供开发者分析性能瓶颈。
3. StrictMode
- 3.1 简介
StrictMode 严格模式,主要用来检测程序中违例情况的开发者工具。最常用的场景就是检测主线程中本地磁盘、网络读写等耗时的操作以及 Activity 泄露等,但该模式不建议在 Release 版本开启,此外该模式无法监控 JNI 中的磁盘 IO 和网络请求且其违例情况仅供参考,需结合实际开发需求予以解决。
主要采用 ThreadPolicy (线程策略) 和 VmPolicy (Vm 策略)进行检测,各策略检测内容如下:
ThreadPolicy
线程策略检测的内容有:
- 自定义的耗时调用使用 detectCustomSlowCalls() 开启。
- 磁盘读取操作使用 detectDiskReads() 开启。
- 磁盘写入操作使用 detectDiskWrites() 开启。
- 网络操作使用 detectNetwork() 开启。
VmPolicy
虚拟机策略检测的内容有:
- Activity 泄露使用 detectActivityLeaks() 开启。
- 未关闭的 closable 对象泄露使用 detectLeakedClosableObjects() 开启。
- 泄露的 sqlite 对象使用 detectLeakedSqlLiteObjects() 开启。
- 检测实例数量使用 setClassInstanceLimit() 开启。
- 3.2 使用
public class DebugUtil {
public static void startStrictModeVmPolicy(){
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectActivityLeaks()/*检测Activity内存泄露*/
.detectLeakedClosableObjects()/*检测未关闭的Closable对象*/
.detectLeakedSqlLiteObjects() /*检测Sqlite对象是否关闭*/
/*也可以采用detectAll()来检测所有想检测的东西*/
.penaltyLog().build());
}
public static void startStrictModeThreadPolicy(){
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()/*磁盘读取操作检测*/
.detectDiskWrites()/*检测磁盘写入操作*/
.detectNetwork() /*检测网络操作*/
/*也可以采用detectAll()来检测所有想检测的东西*/
.penaltyLog().build());
}
}
查看日志输出:
D/StrictMode( 9730): StrictMode policy violation; ~duration=20 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=31 violation=2
D/StrictMode( 9730): at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1176)
D/StrictMode( 9730): at libcore.io.BlockGuardOs.open(BlockGuardOs.java:106)
D/StrictMode( 9730): at libcore.io.IoBridge.open(IoBridge.java:390)
D/StrictMode( 9730): at java.io.FileOutputStream.<init>(FileOutputStream.java:88)
D/StrictMode( 9730): at com.example.strictmodedemo.MainActivity.writeToExternalStorage(MainActivity.java:56)
D/StrictMode( 9730): at com.example.strictmodedemo.MainActivity.onCreate(MainActivity.java:30)
D/StrictMode( 9730): at android.app.Activity.performCreate(Activity.java:4543)