2、Android进程与线程模型

进程和线程,是理解ANR的底层钥匙。很多人在优化ANR时只盯着代码,却忽略了Android系统是怎么管理你的App的。我做个比喻:进程就是一座工厂,线程是工厂里的工人。工厂(进程)有它自己的生命周期,而工人(线程)有各自的职责。工人如果堵在主流水线上,整座工厂就会瘫痪——这就是ANR的根源。

2.1 进程生命周期:你的App能活多久?

Android系统为了省电、保流畅,会随时干掉一些进程。按优先级从高到低,进程分成五种。优先级越高,越不容易被杀死。我刚开始做性能优化时,以为只要App在运行就安全,后来线上出现一堆ANR,才发现后台进程被频繁回收,再启动时主线程卡住了。

优先级等级进程类型典型场景被kill概率
最高前台进程用户正在交互的Activity、正在执行onReceive的广播等几乎不会
高可见进程Activity处于onPause但还能看见(比如半透明弹窗)极低
中服务进程通过startService启动的服务(不绑定UI)较高
低后台进程Activity已onStop,不可见高
最低空进程没有任何组件运行,只作为缓存最先被kill

为什么会这样?因为系统回收内存时,会从最低优先级开始杀。你想想看,如果一个后台进程里有个子线程在做耗时操作,突然被系统回收了,线程还没保存数据——这就是我之前掉过的坑。我曾经在后台服务里做图片批量压缩,结果进程被回收,数据全丢了。后来我改用前台服务+通知,把优先级提到可见进程级别,才稳定下来。

注意:进程被回收后,所有线程都会立刻终止。不要依赖子线程在后台无限执行。可以用WorkManager来做需要保证完成的任务。

2.2 主线程与子线程:谁该干什么?

Android里只有一个主线程,也叫UI线程。它负责三件事:

  • 处理用户触摸、按键等输入事件
  • 执行界面绘制(onDraw、layout等)
  • 分发生命周期回调(onCreate、onResume等)

说白了,主线程就是那条唯一的生产线。一旦这条线被阻塞,系统就会弹ANR对话框。我记得有一次接手一个老项目,主线程里居然做了Bitmap解码——一张3MB的图片解码需要200ms,多来几张,用户多操作几下,ANR就来了。

子线程的职责很明确:做那些主线程不能做的事。比如网络请求、数据库读写、文件IO、大图压缩、JSON解析等。但子线程不能更新UI——这是Android的铁律。为什么?因为多线程同时写UI会导致绘制状态混乱。你想想看,如果两个线程同时改TextView的文本,界面该显示哪个?

我个人的经验法则:任何超过16ms的操作,都应该丢到子线程去。66ms是ANR的阈值,但16ms是流畅的底线。

2.3 Android消息机制:主线程是怎么运转的?

主线程之所以能持续处理任务,靠的是Handler + Looper + MessageQueue这套机制。我画了一张图来展示它们的关系:

Android消息机制核心流程 Android进程 主线程(UI线程) 子线程(Worker线程) Looper.prepare() MessageQueue Looper.loop() → 死循环 Handler(主线程创建) 子线程sendMessage() Message入队 handleMessage() 更新UI(切回主线程) 关键点 主线程Looper由系统自动创建 子线程必须手动调用Looper.prepare()

这套机制怎么工作的?我拆开讲:

  1. Looper:每个线程只能有一个Looper。它负责启动一个死循环,不断从MessageQueue里取消息。主线程的Looper是系统在ActivityThread.main()里自动创建的。
  2. MessageQueue:一个先进先出的消息队列。消息按时间戳排序。如果队列里没消息,Looper就阻塞等待——这样CPU就不会空转。
  3. Handler:向MessageQueue发送消息,并且处理消息。Handler默认绑定创建它的线程的Looper。所以在主线程创建的Handler,handleMessage()就在主线程执行。

说白了,子线程想更新UI,不能直接操作View,必须通过Handler发一个消息到主线程的MessageQueue,然后主线程Looper取出消息,交给Handler处理。我遇到最常见的ANR原因就是:子线程往主线程发了大量Message,或者某个handleMessage里做了耗时操作,导致主线程卡住。

避坑指南:我曾经在handleMessage里直接做了网络请求,导致UI卡了5秒ANR。正确做法是:收到消息后,如果需要复杂逻辑,再开一个子线程去处理,处理好后再发消息回来更新UI。简单点说,就是“子线程干活,主线程只画画”。

代码示例:子线程发消息给主线程

// 主线程创建Handler
Handler mainHandler = new Handler(Looper.getMainLooper()) {
    @Override
    public void handleMessage(Message msg) {
        // 更新UI,必须短平快
        textView.setText(msg.obj.toString());
    }
};

// 子线程里发送消息
new Thread(new Runnable() {
    @Override
    public void run() {
        // 耗时操作,比如下载图片
        Bitmap bitmap = downloadImage(url);
        Message msg = Message.obtain();
        msg.obj = bitmap;
        mainHandler.sendMessage(msg);
    }
}).start();

这段代码很简单,但容易犯两个错:一是忘了用Looper.getMainLooper(),导致handleMessage跑在子线程;二是在子线程里直接new Handler()而不调用Looper.prepare()——这会导致运行时崩溃。你想想看,Android设计这套机制就是为了保证UI线程安全,我们得尊重它。

警告:不要用Handler来传递大量数据或高频消息。如果子线程每秒发几千个Message,主线程handleMessage就会排队,依然会导致ANR。我见过一个案例,子线程用Handler发送位置更新,每秒30次,结果主线程卡了3秒。解决方案是:改用粘性广播或LiveData+协程。

小结:ANR与进程线程的关系

回到ANR监控这个话题上,你需要记住:

  • 进程优先级低 → 容易被杀 → 重新创建时主线程要初始化大量资源 → ANR风险高
  • 主线程做耗时操作 → 阻塞Looper循环 → 系统5秒没收到事件 → ANR
  • 子线程处理不当 → 主线程handlerMessage累积 → 间接导致ANR

接下来我们就要进入实战——如何监控主线程的卡顿。但先别急,先理解这个基础模型,后面讲监控框架时你才能明白每一行代码在做什么。