主线程阻塞与ANR:从根源理解到实战避坑
大家知道,ANR的全称是Application Not Responding。翻译过来就是“应用无响应”。说白了,就是用户点了一下屏幕,等了好久没反应。系统忍了5秒(或更久),觉得你不行了,直接弹个对话框问用户:“要不等,要不关?”
我刚开始接触ANR的时候,总觉得是系统太敏感。后来在一个日活千万的App里踩了无数次坑才明白——主线程阻塞,就是ANR的唯一根源。没有例外。今天这一章,咱们就把主线程为什么会阻塞、阻塞到多严重才算ANR、以及怎么从源头预防,掰开了讲清楚。
主线程到底在干啥?
Android里每个应用都有自己的主线程,也叫UI线程。它负责三件大事:
- 处理输入事件:触摸、按键、滑动
- 执行四大组件的生命周期:Activity.onCreate、BroadcastReceiver.onReceive 等等
- 刷新界面:执行onDraw、layout、measure
你想想看,如果主线程被堵住了,这三件事全卡住。用户点了个按钮,事件排着队等;Activity要启动,onCreate等;界面要显示一张图,onDraw等。等超过5秒,系统就不干了——ANR诞生。
我记得有一次线上反馈说App启动后点“登录”按钮卡死。后来发现是登录Activity的onCreate里做了一次网络请求,而且没设超时——服务器恰好挂了。那一次我记住了:任何可能导致阻塞的代码,都不该出现在主线程。
哪些操作会阻塞主线程?——一张表看懂
| 操作类型 | 常见例子 | 后果 |
|---|---|---|
| 网络请求 | HttpURLConnection.connect()、OkHttp同步调用 | 若网络慢或无响应,主线程挂起数秒至数十秒 |
| 磁盘I/O | SharedPreferences.commit()大文件、SQLite大量写入、Bitmap decode | 磁盘抖动时,一次I/O可能超过100ms,累积超时 |
| 对象创建/GC | 频繁new大对象、循环创建导致GC停顿 | GC时所有线程暂停,主线程无法处理事件 |
| 锁等待 | 主线程获取一个被其他线程持有的锁 | 死锁或严重锁竞争导致主线程不响应 |
| 布局/渲染 | 嵌套过深的RelativeLayout、过度绘制、View#postDelay | 帧率下降,累积超过5秒触发ANR |
这张表我建议你存一下。每次遇到ANR,先用这个列表排查项目代码里的主线程操作。我个人习惯是直接在代码里搜所有在主线程调用的网络、文件读写、数据库操作,基本一抓一个准。
ANR触发的时间阈值——别记错
不同的组件和场景,系统给的“耐心”不一样。我整理了一下,你们直接看:
| 场景 | 超时时间 | 关键说明 |
|---|---|---|
| 输入事件(按键/触摸) | 5秒 | 如果5秒内没响应输入事件,系统判定ANR |
| BroadcastReceiver(前台) | 10秒 | 前台广播接收器的onReceive必须在10秒内返回 |
| BroadcastReceiver(后台) | 60秒 | 后台广播接收器超时时间较长,但依然会触发 |
| Service(前台) | 20秒 | onStartCommand等方法的执行时间限制 |
| ContentProvider | 10秒 | 查询或者更新操作不能超过10秒 |
一个真实的ANR案例拆解
去年我一个项目上线前压测,发现一个诡异ANR:用户连续快速点击“上传图片”按钮,偶发ANR。我拉下/data/anr/traces.txt,看到主线程堆栈停在:
at android.os.MessageQueue.nativePollOnce
at android.os.MessageQueue.next
at android.os.Looper.loop
at android.app.ActivityThread.main
嗯,看着好像没阻塞?但关键在后面的子线程堆栈:一个线程持有锁在写文件,另一个线程也在等同一把锁。而主线程恰好也调用了需要这把锁的方法——锁链式阻塞。
排查后发现:上传图片时主线程调用了getSharedPreferences()(内部有锁),而两个工作线程恰好在高并发写入SharedPreferences,导致主线程等锁超5秒。解决方案很简单:把SharedPreferences放到子线程的postExecute里,或者改用DataStore。
预防主线程阻塞的三大原则
- 原则一:凡耗时超过16ms的任务,必须扔到子线程(16ms对应60fps的渲染周期)
- 原则二:四大组件的生命周期方法里只做轻量工作——比如发个EventBus事件、startService,不做网络或文件
- 原则三:所有跨线程调用涉及锁的地方,用StrictMode或CheckThread工具标记,让编译期告诉你哪里违规了
嗯,我再说个个人经验:其实很多ANR不是立刻爆发的。比如你一个ListView里每项图片都decode一遍,可能每帧只卡20ms,但累计到第300帧时,系统发现你已经有5秒没处理输入了——啪,ANR。所以不仅要避免大阻塞,还要消除小积压。
本章知识体系总览
总结
主线程阻塞与ANR的关系,说白了就是“一根独木桥被堵死了”。想要不触发ANR,就记住一个核心思路:主线程只做轻量UI工作,其他都交给子线程。同时要学会看traces.txt,定位主线程到底卡在哪个函数。这一章的内容是基础,也是后续所有ANR治理手段的源头。