第26章 进程与线程管理:Android进程模型、线程优先级与调度、cgroup与进程组、ANR触发机制与日志分析
各位同学,今天我们来聊聊Android系统里最核心的“骨架”——进程与线程管理。说实话,这部分内容在面试中几乎是必考题,而且在实际开发中,很多疑难杂症都跟它有关。我当年刚接触Android Framework时,就被ANR折磨得够呛,后来才明白,不懂底层调度机制,你连日志都看不懂。
一、Android进程模型:Linux进程的“变种”
Android底层跑的是Linux内核,所以进程模型本质上就是Linux进程。但Google做了不少“魔改”,让它更适合移动端。
进程的创建:在Linux里,fork()是标准做法。Android呢?它用的是Zygote进程孵化机制。Zygote本身是一个Java进程,系统启动时由init进程创建。之后所有应用进程,都是Zygote fork出来的。
我给大家画个图,看看这个流程:
Zygote为什么这么设计?说白了,就是为了加速。Zygote在启动时就把常用的Java类、资源都加载好了,fork出来的子进程直接继承这些数据。加上Linux的写时拷贝技术,内存占用也小。我在项目中遇到过一个问题:某款低端手机启动应用特别慢,后来发现是Zygote堆内存设置太小,预加载的类不够多,每次fork后还得自己加载一堆东西。
二、线程优先级与调度:谁先跑,谁等着
Android里的线程调度,底层走的是Linux的CFS(完全公平调度器)。但Android在上面又封装了一层,叫“Android调度策略”。
线程优先级:Android定义了从-20到19的nice值,数值越小优先级越高。但咱们开发者一般不直接操作nice值,而是用Android提供的API:
// 设置线程优先级
android.os.Process.setThreadPriority(
android.os.Process.THREAD_PRIORITY_BACKGROUND
);
// 对应的nice值映射
THREAD_PRIORITY_DEFAULT // 0
THREAD_PRIORITY_BACKGROUND // 10
THREAD_PRIORITY_FOREGROUND // -2
THREAD_PRIORITY_DISPLAY // -4
THREAD_PRIORITY_URGENT_DISPLAY // -8
THREAD_PRIORITY_AUDIO // -16
嗯,这里要注意:UI线程的优先级是THREAD_PRIORITY_DEFAULT(0),而渲染线程是THREAD_PRIORITY_DISPLAY(-4)。所以渲染线程比UI线程优先级高,这是为了确保画面流畅。我见过有人把后台网络请求线程设成THREAD_PRIORITY_URGENT_DISPLAY,结果UI卡顿得一塌糊涂——你想想看,一个下载线程跟渲染线程抢CPU,画面能流畅才怪。
调度组:Android把线程分成几个调度组:
- Top App:前台应用,优先级最高
- Foreground:前台服务等
- Background:后台进程,CPU时间片被大幅限制
- System:系统服务
每个调度组对应不同的cgroup配置,这个我们下面细说。
三、cgroup与进程组:资源隔离的“铁笼子”
cgroup是Linux内核的一个特性,用来限制、隔离进程组的资源使用。Android从4.3开始引入cgroup,现在已经是资源管理的核心手段。
Android主要用cgroup来控制:
| cgroup子系统 | 控制内容 | Android中的应用 |
|---|---|---|
| cpu | CPU时间片分配 | 限制后台进程CPU使用 |
| cpuacct | CPU使用统计 | 统计每个应用的CPU消耗 |
| memory | 内存使用上限 | 防止单个应用耗尽内存 |
| blkio | I/O带宽 | 限制后台进程磁盘读写 |
| freezer | 进程冻结/解冻 | 应用进入后台时冻结 |
我给大家画个cgroup的层级结构:
我曾经在分析一个系统卡顿问题时,发现后台进程的cpu.shares被设成了100,而前台是1024。这意味着后台进程最多只能拿到约9%的CPU时间。但有个流氓应用通过创建多个进程来绕过限制——每个进程单独算,加起来就多了。后来Android在cgroup里加了进程组的概念,把同一个应用的所有进程归到一个组里统一限制。
核心要点:cgroup是Android资源隔离的基石。前台应用和后台应用的CPU、内存、I/O待遇天差地别。这也是为什么你的应用切到后台后,网络请求会变慢、动画会卡顿。
四、ANR触发机制与日志分析
ANR(Application Not Responding)是每个Android开发者都绕不开的噩梦。说白了,就是系统觉得你的应用“没反应”了,弹个对话框让用户选择“继续等”还是“关掉”。
ANR的触发条件:
- 输入事件超时:5秒内没有处理完(比如按键、触摸)
- 广播超时:前台广播10秒,后台广播60秒
- 服务超时:前台服务20秒,后台服务200秒
- ContentProvider超时:10秒内没有发布
为什么会超时?我总结了几种常见情况:
- 主线程做了耗时操作:比如网络请求、大文件读写、数据库操作
- 锁竞争:主线程在等一个锁,而持有锁的线程在干别的事
- Binder调用阻塞:主线程调用了Binder同步方法,对方没响应
- CPU被抢:其他进程占用了太多CPU,主线程分不到时间片
日志分析实战:
当ANR发生时,系统会在/data/anr/traces.txt里生成一份dump文件。我教大家怎么看:
----- pid 12345 at 2024-01-15 10:30:00 -----
Cmd line: com.example.myapp
DALVIK THREADS:
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x751234
| sysTid=12345 nice=0 cgrp=top-app
| schedstat=( 12345678 23456789 123 )
at android.os.MessageQueue.nativePollOnce(Native Method)
at android.os.MessageQueue.next(MessageQueue.java:325)
at android.os.Looper.loop(Looper.java:142)
at android.app.ActivityThread.main(ActivityThread.java:6934)
"Binder:12345_2" prio=5 tid=15 Blocked
| group="main" sCount=1 dsCount=0 flags=1 obj=0x752345
| sysTid=12347 nice=0 cgrp=top-app
at com.example.MyService.doSomething(MyService.java:50)
- waiting to lock <0x07654321> (a java.lang.Object)
at com.example.MyService$1.run(MyService.java:30)
at android.os.Handler.handleCallback(Handler.java:883)
看日志的关键点:
- 看主线程状态:上面主线程是Native状态,在等消息,这其实是正常的。如果主线程是Blocked或Runnable,那就有问题。
- 看锁等待:Binder线程显示“waiting to lock”,说明它在等一把锁。谁拿着这把锁?得往上翻找持有锁的线程。
- 看schedstat:这个字段记录了CPU时间。如果主线程的CPU时间很少,说明它没抢到时间片,可能是系统负载太高。
我的经验:遇到ANR,先别急着看代码。先看traces.txt里主线程在干什么。如果是Blocked,找锁;如果是Runnable,看它在执行什么方法;如果是Native,可能是Binder调用卡住了。我曾经遇到一个ANR,主线程在等一个数据库锁,而那个锁被一个后台线程拿着,后台线程又在等网络响应——典型的死锁场景。
避坑指南:不要在主线程做任何可能阻塞的操作。SharedPreferences的写入、数据库查询、网络请求,统统放到子线程。如果你用了ContentProvider,注意它的query方法默认跑在调用者的Binder线程池里,如果调用者是主线程,那query里也不能做耗时操作。
最后说一句,ANR日志分析是个熟能生巧的活。我建议大家在开发阶段就开启“严格模式”:
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build());
}
这样能在开发阶段就发现潜在问题,而不是等到线上用户反馈ANR才去查日志。好了,进程与线程管理这块内容,核心就是理解Android怎么创建进程、怎么调度线程、怎么隔离资源、怎么检测卡顿。把这些搞明白了,你离Android Framework专家又近了一步。
公众号:蓝海资料掘金营,微信deep3321