2、Android进程与线程模型
进程和线程,是理解ANR的底层钥匙。很多人在优化ANR时只盯着代码,却忽略了Android系统是怎么管理你的App的。我做个比喻:进程就是一座工厂,线程是工厂里的工人。工厂(进程)有它自己的生命周期,而工人(线程)有各自的职责。工人如果堵在主流水线上,整座工厂就会瘫痪——这就是ANR的根源。
2.1 进程生命周期:你的App能活多久?
Android系统为了省电、保流畅,会随时干掉一些进程。按优先级从高到低,进程分成五种。优先级越高,越不容易被杀死。我刚开始做性能优化时,以为只要App在运行就安全,后来线上出现一堆ANR,才发现后台进程被频繁回收,再启动时主线程卡住了。
| 优先级等级 | 进程类型 | 典型场景 | 被kill概率 |
|---|---|---|---|
| 最高 | 前台进程 | 用户正在交互的Activity、正在执行onReceive的广播等 | 几乎不会 |
| 高 | 可见进程 | Activity处于onPause但还能看见(比如半透明弹窗) | 极低 |
| 中 | 服务进程 | 通过startService启动的服务(不绑定UI) | 较高 |
| 低 | 后台进程 | Activity已onStop,不可见 | 高 |
| 最低 | 空进程 | 没有任何组件运行,只作为缓存 | 最先被kill |
为什么会这样?因为系统回收内存时,会从最低优先级开始杀。你想想看,如果一个后台进程里有个子线程在做耗时操作,突然被系统回收了,线程还没保存数据——这就是我之前掉过的坑。我曾经在后台服务里做图片批量压缩,结果进程被回收,数据全丢了。后来我改用前台服务+通知,把优先级提到可见进程级别,才稳定下来。
2.2 主线程与子线程:谁该干什么?
Android里只有一个主线程,也叫UI线程。它负责三件事:
- 处理用户触摸、按键等输入事件
- 执行界面绘制(onDraw、layout等)
- 分发生命周期回调(onCreate、onResume等)
说白了,主线程就是那条唯一的生产线。一旦这条线被阻塞,系统就会弹ANR对话框。我记得有一次接手一个老项目,主线程里居然做了Bitmap解码——一张3MB的图片解码需要200ms,多来几张,用户多操作几下,ANR就来了。
子线程的职责很明确:做那些主线程不能做的事。比如网络请求、数据库读写、文件IO、大图压缩、JSON解析等。但子线程不能更新UI——这是Android的铁律。为什么?因为多线程同时写UI会导致绘制状态混乱。你想想看,如果两个线程同时改TextView的文本,界面该显示哪个?
2.3 Android消息机制:主线程是怎么运转的?
主线程之所以能持续处理任务,靠的是Handler + Looper + MessageQueue这套机制。我画了一张图来展示它们的关系:
这套机制怎么工作的?我拆开讲:
- Looper:每个线程只能有一个Looper。它负责启动一个死循环,不断从MessageQueue里取消息。主线程的Looper是系统在ActivityThread.main()里自动创建的。
- MessageQueue:一个先进先出的消息队列。消息按时间戳排序。如果队列里没消息,Looper就阻塞等待——这样CPU就不会空转。
- Handler:向MessageQueue发送消息,并且处理消息。Handler默认绑定创建它的线程的Looper。所以在主线程创建的Handler,handleMessage()就在主线程执行。
说白了,子线程想更新UI,不能直接操作View,必须通过Handler发一个消息到主线程的MessageQueue,然后主线程Looper取出消息,交给Handler处理。我遇到最常见的ANR原因就是:子线程往主线程发了大量Message,或者某个handleMessage里做了耗时操作,导致主线程卡住。
代码示例:子线程发消息给主线程
// 主线程创建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线程安全,我们得尊重它。
小结:ANR与进程线程的关系
回到ANR监控这个话题上,你需要记住:
- 进程优先级低 → 容易被杀 → 重新创建时主线程要初始化大量资源 → ANR风险高
- 主线程做耗时操作 → 阻塞Looper循环 → 系统5秒没收到事件 → ANR
- 子线程处理不当 → 主线程handlerMessage累积 → 间接导致ANR
接下来我们就要进入实战——如何监控主线程的卡顿。但先别急,先理解这个基础模型,后面讲监控框架时你才能明白每一行代码在做什么。