10.03 ANR 与主线程规则

2025/3/12 Android 开发基础

前两节建立了标准范式:耗时任务去线程池,结果用 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收集)

分析步骤:

  1. 打开 traces 找到 ----- pid xxxxx at ... ----- 对应你应用的进程段
  2. 看名为 "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)   ← 卡住的位置
  1. 常见堆栈特征速查:
堆栈特征 典型原因
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 高发窗口:

  1. Application.onCreate 瘦身:SDK 初始化按需延迟——非首页必需的 SDK 移到首个页面空闲时(MessageQueue.addIdleHandler,见 10.01 第六节)或后台线程
  2. 首屏轻量:首页布局层级控制(避免深层嵌套),首帧前不做磁盘 IO
  3. 第三方库异步初始化:有主线程初始化要求的库(如地图)放最后,或用启动器框架(如 Android Startup)编排依赖
  4. 严查主线程 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();
    }
}

# 六、常见问题与陷阱

  1. "主线程 sleep 2 秒会不会 ANR?"——不一定立刻 ANR:只要没有输入事件/生命周期消息在等待(页面空闲),系统不会判超时;但一旦用户点击,输入事件排在 sleep 后面 5 秒内处理不完就 ANR。不要用"没崩溃"当"没问题"的证据。
  2. ANR 偶发难复现:多为"主线程本就繁忙 + 偶发耗时"叠加(如低端机 + SP 首次读盘 + 事件),用 StrictMode 常开 + 线上 APM 覆盖。
  3. onReceive 里做异步就万事大吉?——注意:BroadcastReceiver 的 goAsync() 只能延后系统计时约 10 秒,把大任务转线程池才是正解;且 onReceive 结束后进程优先级下降,后台任务可能被杀,关键任务走 WorkManager。
  4. 子线程更新 UI 一定崩?——不一定立刻崩:checkThread 在 ViewRootImpl 创建后才生效,子线程在视图未 attach 前设置属性不报错,属于"偶然不崩",仍然违规。
  5. 对话框、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)。