第4章 主线程耗时操作检测归纳总结

说到 ANR,十有八九跟主线程脱不了干系。你想想,用户点了个按钮,等了五秒没反应,系统直接弹窗“App 未响应”。这时候你脑子里应该立刻蹦出一个问题:主线程到底在干啥?

这一章,我就把主线程上常见的耗时操作给你捋一遍。哪些操作容易踩坑?怎么检测?日志里怎么看?咱们一个个说清楚。

核心观点:主线程不是用来干活的,是用来响应的。任何超过 5 秒的任务都可能触发 ANR,但 100ms 以上的卡顿就值得警觉。

常见耗时操作分类

我个人习惯把主线程的“雷区”分成五大类。遇到 ANR 时,先按这个清单排查,效率高很多。

类别典型场景为什么危险
文件/网络 IOSharedPreferences 写操作、网络请求、Bitmap 解码IO 等待不可控,尤其磁盘慢或网络超时时可能阻塞数秒
计算密集型List 排序、JSON 解析、图片裁剪纯 CPU 运算,数据量大时轻松吃掉几百毫秒甚至几秒
锁竞争主线程持锁等待子线程释放锁子线程没来得及释放,主线程直接卡死
反射/序列化Gson/FastJson 大对象、反射调用频繁反射本身慢,序列化大 JSON 内存分配频繁,容易 GC 抖动
UI 布局复杂 RecyclerView 嵌套、动态 inflate 大布局measure/layout 耗时可能超过 16ms,多次累积导致 ANR
避坑指南:我曾经接手一个项目,用户反馈拍照后经常 ANR。查日志发现主线程在做 BitmapFactory.decodeStream,直接解码一张 4000x3000 的原图,耗时 3.8 秒。后来改用 inSampleSize 采样,再放到子线程,问题解决。

检测方法,四种常用手段

知道了哪些操作容易出问题,接下来就得会检测。我平时主要用这四种方法,各有侧重。

  1. Looper Printer 打点 —— 最轻量,适合线上埋点
  2. BlockCanary —— 开发阶段实时查看卡顿堆栈
  3. StrictMode —— 系统级违规检测,自动弹出警告
  4. 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% 的性能开销。线上不推荐,线下分析利器。

注意:使用 TraceView 时,建议只录制 3-5 秒。录制时间太长文件会非常大,而且分析时容易走神。我只录制用户反馈的复现步骤,精准定位。

日志定位技巧

当你拿到一个 ANR 日志,比如 traces.txt 或者 Bugly 上报的堆栈,怎么快速判断是主线程耗时操作导致的?我给你三个步骤。

  1. 找主线程标记:日志里搜索 "main" prio=5,这是主线程的典型标识。
  2. 看线程状态:如果是 RUNNABLE,说明正在执行代码;如果是 BLOCKED,那就是在等锁;如果是 WAITING,可能在等网络或 sleep。
  3. 看调用栈顶部:顶部是当前正在执行的方法。如果顶部是 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 贴在显示器旁边。

主线程耗时操作检测知识体系 主线程耗时操作 IO操作 计算密集型 锁竞争 反射/序列化 Looper Printer BlockCanary StrictMode Profiler ANR日志定位 查找主线程堆栈 分析线程状态 识别调用栈顶部 提示:先分类定位耗时操作类型,再选择合适的检测工具

总结对比表

最后给你一张表,方便你根据场景选择检测手段。

检测手段检测范围性能影响适用阶段
Looper Printer消息粒度,可自定义阈值极低线上/线下
BlockCanary自动采集堆栈,精确到函数较低开发/测试
StrictMode磁盘IO、网络、资源未关闭低开发
CPU Profiler所有函数耗时占比10%~30%线下分析
再次提醒:不要在一个项目里同时开启 BlockCanary 和 StrictMode,两者都会在主线程做 Hook,组合使用时可能互相影响,导致崩溃。我见过同时开启后,某次磁盘读写触发了双重警告,堆栈混淆,反而更难排查。

嗯,这一章就到这里。你只要把上面的分类和检测手段记住,碰到 ANR 先问自己一句:“主线程现在在做什么?”——答案往往就在那一行堆栈里。