第4章 主线程耗时操作检测归纳总结
说到 ANR,十有八九跟主线程脱不了干系。你想想,用户点了个按钮,等了五秒没反应,系统直接弹窗“App 未响应”。这时候你脑子里应该立刻蹦出一个问题:主线程到底在干啥?
这一章,我就把主线程上常见的耗时操作给你捋一遍。哪些操作容易踩坑?怎么检测?日志里怎么看?咱们一个个说清楚。
常见耗时操作分类
我个人习惯把主线程的“雷区”分成五大类。遇到 ANR 时,先按这个清单排查,效率高很多。
| 类别 | 典型场景 | 为什么危险 |
|---|---|---|
| 文件/网络 IO | SharedPreferences 写操作、网络请求、Bitmap 解码 | IO 等待不可控,尤其磁盘慢或网络超时时可能阻塞数秒 |
| 计算密集型 | List 排序、JSON 解析、图片裁剪 | 纯 CPU 运算,数据量大时轻松吃掉几百毫秒甚至几秒 |
| 锁竞争 | 主线程持锁等待子线程释放锁 | 子线程没来得及释放,主线程直接卡死 |
| 反射/序列化 | Gson/FastJson 大对象、反射调用频繁 | 反射本身慢,序列化大 JSON 内存分配频繁,容易 GC 抖动 |
| UI 布局 | 复杂 RecyclerView 嵌套、动态 inflate 大布局 | measure/layout 耗时可能超过 16ms,多次累积导致 ANR |
BitmapFactory.decodeStream,直接解码一张 4000x3000 的原图,耗时 3.8 秒。后来改用 inSampleSize 采样,再放到子线程,问题解决。
检测方法,四种常用手段
知道了哪些操作容易出问题,接下来就得会检测。我平时主要用这四种方法,各有侧重。
- Looper Printer 打点 —— 最轻量,适合线上埋点
- BlockCanary —— 开发阶段实时查看卡顿堆栈
- StrictMode —— 系统级违规检测,自动弹出警告
- TraceView / Profiler —— 深入分析函数耗时分布
1. Looper Printer
原理很简单:给主线程 Looper 设置一个 Printer,每次消息执行前后都会回调。如果前后时间差超过阈值(比如 2 秒),就说明卡顿了。
Looper.getMainLooper().setMessageLogging(new Printer() {
@Override
public void println(String x) {
if (x.startsWith(">>>>")) {
startTime = System.currentTimeMillis();
} else if (x.startsWith("<<<<")) {
long cost = System.currentTimeMillis() - startTime;
if (cost > 2000) {
// 记录堆栈
Log.w("ANR", "主线程卡顿 " + cost + "ms");
Log.w("ANR", Log.getStackTraceString(new Throwable()));
}
}
}
});
注意:这个方法只能知道“消息”级别的卡顿,无法精确到某个函数内部。但作为线上监控的兜底方案,性价比很高。
2. BlockCanary
BlockCanary 其实是对 Looper Printer 的封装,但它能自动采集当时的线程堆栈。我建议在 debug 包开启,release 包按需启用。
BlockCanary.install(this, new BlockCanaryContext() {
@Override
public int provideBlockThreshold() {
return 1000; // 1 秒以上算卡顿
}
}).start();
堆栈信息会直接打印到 Logcat,或者写入文件。看到堆栈里有 android.os.MessageQueue.nativePollOnce 别慌,那只是 idle 状态;真正有问题的往往是 android.widget、java.io 这类包名下的调用。
3. StrictMode
StrictMode 是 Android 官方提供的检测工具。你可以在 Application.onCreate 里开启,注意只在 debug 构建使用。
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build());
}
一旦主线程做了磁盘 IO 或网络请求,Logcat 就会输出 StrictMode 开头的警告。不过 StrictMode 只能检测调用了系统 API 的违规操作,如果耗时操作是纯计算,它无能为力。
4. TraceView / CPU Profiler
当卡顿已经发生,你想知道到底是哪个函数最耗时,就得用 Profiler。Android Studio 自带的 CPU Profiler 很好用,但注意它本身会带来 10%-30% 的性能开销。线上不推荐,线下分析利器。
日志定位技巧
当你拿到一个 ANR 日志,比如 traces.txt 或者 Bugly 上报的堆栈,怎么快速判断是主线程耗时操作导致的?我给你三个步骤。
- 找主线程标记:日志里搜索
"main" prio=5,这是主线程的典型标识。 - 看线程状态:如果是
RUNNABLE,说明正在执行代码;如果是BLOCKED,那就是在等锁;如果是WAITING,可能在等网络或 sleep。 - 看调用栈顶部:顶部是当前正在执行的方法。如果顶部是
Process.readFully或FileInputStream.readBytes,八成是 IO 阻塞。
举个例子:
"main" prio=5 tid=1 RUNNABLE
at java.io.FileInputStream.readBytes(Native Method)
at java.io.FileInputStream.read(FileInputStream.java:285)
at libcore.io.Streams.readFully(Streams.java:83)
at java.io.FileInputStream.read(FileInputStream.java:261)
at android.content.ContentResolver.openInputStream(ContentResolver.java:1456)
at com.example.MainActivity.loadData(MainActivity.java:89)
at com.example.MainActivity.onCreate(MainActivity.java:42)
...
看到 readBytes 在最上面,而且整个堆栈都没出应用包名?那就是你代码里在主线程读文件了,赶紧移出去。
知识结构图
下面这张图总结了主线程耗时操作检测的完整链路,从分类到检测再到定位。你可以把它当作排查 checklist 贴在显示器旁边。
总结对比表
最后给你一张表,方便你根据场景选择检测手段。
| 检测手段 | 检测范围 | 性能影响 | 适用阶段 |
|---|---|---|---|
| Looper Printer | 消息粒度,可自定义阈值 | 极低 | 线上/线下 |
| BlockCanary | 自动采集堆栈,精确到函数 | 较低 | 开发/测试 |
| StrictMode | 磁盘IO、网络、资源未关闭 | 低 | 开发 |
| CPU Profiler | 所有函数耗时占比 | 10%~30% | 线下分析 |
嗯,这一章就到这里。你只要把上面的分类和检测手段记住,碰到 ANR 先问自己一句:“主线程现在在做什么?”——答案往往就在那一行堆栈里。