10.03 ANR 与主线程规则
前两节建立了标准范式:耗时任务去线程池,结果用 Handler 切回主线程。本节回答反面问题:如果违反规则会发生什么——这就是 ANR(Application Not Responding,应用无响应),Android 对主线程的"超时保护机制"。用户看到的是弹窗"应用未响应,等待/关闭",对产品口碑伤害很大。
# 一、ANR 的触发条件
系统对主线程的不同消息类型设定了不同的超时上限,超时即触发 ANR:
| 消息类型 | 超时时间 | 说明 |
|---|---|---|
| 输入事件(点击/按键) | 5 秒内未处理完 | InputDispatchingTimeout,最常见(Input dispatching timed out (Waiting because the touched window has not finished processing the input events that were previously submitted to it.)) |
| 前台 Service | 20 秒内未执行完 onCreate/onStartCommand | |
| 后台 Service | 200 秒 | |
| 前台广播 BroadcastReceiver | 10 秒内 onReceive 未结束 | |
| 后台广播 | 60 秒 | |
| ContentProvider | publish 后 10 秒内未就绪 |
共同本质:某条必须在主线程处理的消息(输入事件、生命周期、onReceive)在队列里排了队、但前面的某条消息把它堵住太久(或者它自己执行太久),超过了系统时限。
ANR 的检测机制与 10.01 的消息机制直接相关:系统在分发输入事件/生命周期消息时,同步地插入一条"定时消息"(延时后向系统报告 ANR),主线程处理完目标任务后会移除它;超时未移除即判定 ANR。这也解释了 ANR 统计的是"单条消息的处理时长",而不是"主线程总忙碌时长"。
# 二、ANR 的定位方法:traces 文件
ANR 发生时系统会抓取全进程线程堆栈写入 traces 文件,这是定位的主战场:
/data/anr/traces.txt(需要root或调试设备访问;线上用Firebase/友盟等APM收集)
分析步骤:
- 打开 traces 找到
----- pid xxxxx at ... -----对应你应用的进程段 - 看名为 "main" 的线程堆栈(ANR 时主线程卡在哪一帧)
// 典型案例:主线程做网络请求的堆栈特征
"main" prio=5 tid=1 Sleeping
at java.lang.Thread.sleep(Thread.java)
at okhttp3.internal.Util.sleep(...)
at com.example.DemoActivity.onCreate(DemoActivity.java:38) ← 卡住的位置
- 常见堆栈特征速查:
| 堆栈特征 | 典型原因 |
|---|---|
Thread.sleep / Object.wait 在 Activity 代码里 | 主线程 sleep/等待 |
Socket.connect / OkHttp.execute | 主线程网络请求 |
SQLiteDatabase.query/insert | 主线程数据库操作 |
SharedPreferences$EditorImpl.commit | 主线程同步写 SP |
BitmapFactory.decodeResource | 主线程解码大图 |
等待锁(monitor enter) | 主线程等子线程持有的锁(锁倒挂) |
# 三、主线程操作清单:什么能做,什么禁止
# 3.1 主线程可以做的(耗时可控)
- View 的创建、属性设置、增删(UI 操作本身,毫秒级)
- 轻量
SharedPreferences.getString读(首次加载进内存后是内存 Map 查询) notifyDataSetChanged、requestLayout 等刷新调度- 小对象构造、字符串拼接等常规逻辑
# 3.2 主线程禁止的(红线)
| 操作 | 原因 | 正确姿势 |
|---|---|---|
| 网络请求(HttpURLConnection/OkHttp.execute) | 延迟不可控,且 Android 4.0 起直接抛 NetworkOnMainThreadException | Retrofit/OkHttp 异步 |
| SQLite 读写(尤其写入) | 单条可能几毫秒,批量写入几十毫秒起步 | Room + 协程/线程池 |
SharedPreferences.commit() | 同步落盘(见 09 数据存储) | apply() |
| 大文件读写、Bitmap 解码 | 大图解码可达百毫秒级 | Glide/IO线程 |
Thread.sleep / 长 Object.wait | 纯阻塞 | postDelayed |
| 遍历超大集合做复杂计算 | CPU 长时间被占 | 线程池 |
| 等待子线程的结果(无超时的 join/锁) | 锁倒挂:主线程等持有锁的子线程 | 异步回调模型 |
binder 调用也要警惕:
getInstalledPackages、getRunningTasks等跨进程 API 本质是 IPC,对方进程慢你就跟着慢,高频调用同样会堵主线程。
# 四、耗时任务的正确处理模式
把前面几节串成标准范式——线程池执行 + Handler 回主线程:
public class DemoActivity extends AppCompatActivity {
private TextView mTextView;
// 静态内部类Handler + 弱引用(防泄漏,见10.01第七节)
private static class UiHandler extends Handler {
private final WeakReference<DemoActivity> mActivityRef;
UiHandler(DemoActivity activity) {
super(Looper.getMainLooper());
mActivityRef = new WeakReference<>(activity);
}
@Override
public void handleMessage(Message msg) {
DemoActivity activity = mActivityRef.get();
if (activity == null || activity.isFinishing() || activity.isDestroyed()) {
return;
}
switch (msg.what) {
case 1:
// (String)msg.obj为后台查询结果
activity.mTextView.setText((String) msg.obj);
break;
default:
break;
}
}
}
private final Handler mUiHandler = new UiHandler(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_demo);
mTextView = findViewById(R.id.tv_result);
findViewById(R.id.btn_query).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mTextView.setText("查询中...");
// ① 耗时操作进线程池(10.02)
ThreadPoolManager.execute(new Runnable() {
@Override
public void run() {
String result = queryDatabase(); // 子线程:SQLite查询
// ② 结果切回主线程(10.01)
Message msg = Message.obtain(mUiHandler, 1);
msg.obj = result;
mUiHandler.sendMessage(msg);
}
});
}
});
}
// 子线程执行的数据库查询
private String queryDatabase() {
MyDatabaseHelper helper = new MyDatabaseHelper(this);
SQLiteDatabase db = helper.getReadableDatabase();
Cursor cursor = db.rawQuery("SELECT name FROM user LIMIT 1", null);
try {
return cursor.moveToNext() ? cursor.getString(0) : "无数据";
} finally {
cursor.close();
}
}
}
这个模式就是 AsyncTask/Glide/Room + Flow 内部共同的骨架。理解了它,任何异步框架都是语法糖。
# 五、启动阶段的特殊优化:避免"冷启动 ANR 风险"
应用冷启动时主线程任务密集(Application.onCreate → Activity.onCreate → 首帧绘制),是 ANR 高发窗口:
- Application.onCreate 瘦身:SDK 初始化按需延迟——非首页必需的 SDK 移到首个页面空闲时(
MessageQueue.addIdleHandler,见 10.01 第六节)或后台线程 - 首屏轻量:首页布局层级控制(避免深层嵌套),首帧前不做磁盘 IO
- 第三方库异步初始化:有主线程初始化要求的库(如地图)放最后,或用启动器框架(如 Android Startup)编排依赖
- 严查主线程 IO:开发期开启
StrictMode(见下)抓现行
// 开发期开启StrictMode:主线程IO/网络操作直接打日志(甚至崩溃提醒)
public class MyApp extends Application {
@Override
public void onCreate() {
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll() // 检测主线程磁盘读/写/网络等
.penaltyLog() // 打印日志(可加penaltyDeath显式暴露)
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedClosableObjects()
.penaltyLog()
.build());
}
super.onCreate();
}
}
# 六、常见问题与陷阱
- "主线程 sleep 2 秒会不会 ANR?"——不一定立刻 ANR:只要没有输入事件/生命周期消息在等待(页面空闲),系统不会判超时;但一旦用户点击,输入事件排在 sleep 后面 5 秒内处理不完就 ANR。不要用"没崩溃"当"没问题"的证据。
- ANR 偶发难复现:多为"主线程本就繁忙 + 偶发耗时"叠加(如低端机 + SP 首次读盘 + 事件),用 StrictMode 常开 + 线上 APM 覆盖。
- onReceive 里做异步就万事大吉?——注意:BroadcastReceiver 的
goAsync()只能延后系统计时约 10 秒,把大任务转线程池才是正解;且 onReceive 结束后进程优先级下降,后台任务可能被杀,关键任务走 WorkManager。 - 子线程更新 UI 一定崩?——不一定立刻崩:
checkThread在 ViewRootImpl 创建后才生效,子线程在视图未 attach 前设置属性不报错,属于"偶然不崩",仍然违规。 - 对话框、Toast 在 Service 里弹:Service 没有窗口上下文(Android 11+ 后台 Toast 被限制),且非主线程弹 Toast 需要 Looper——统一约定:一切 UI 反馈都回到主线程。
# 结束
本节介绍了 ANR 与主线程规则:ANR 的触发条件表(输入 5s/前台 Service 20s/前台广播 10s 等)与"单条消息处理超时"的本质(与 10.01 的消息机制直接相关)、traces.txt 定位法(main 线程堆栈特征速查表)、主线程可做/禁止清单(网络/SQLite/commit/大图解码四大红线)、耗时任务的标准处理范式(线程池 + Handler 回主线程,即 AsyncTask/Glide 的内部骨架)、启动阶段的 ANR 防范(IdleHandler 延迟初始化 + StrictMode)。至此异步篇完成:为什么异步(10.00)→ 怎么切线程(10.01)→ 工作线程怎么管(10.02)→ 违反规则的代价(本文)。接下来进入数据篇:11 网络编程。更多内容请参考:应用无响应 (opens new window)。