2. Android进程模型
Android 的进程模型并不是凭空设计的,它建立在 Linux 进程机制之上,又针对移动设备的资源约束与安全需求做了大量定制。理解这一层模型,是理解 Binder、共享内存、AIDL 等跨进程通信技术的前提。本节从 Linux 进程基础出发,逐层剖析 Android 应用进程的创建、优先级调度、隔离与沙箱机制。
一、Linux进程基础:一切皆进程
在 Linux 中,进程是资源分配与调度的基本单位。每个进程拥有独立的虚拟地址空间、文件描述符表、信号处理表以及一组线程。内核通过 task_struct 描述进程,通过 fork() 或 clone() 创建新进程,通过 exec() 加载可执行映像。进程之间默认彼此隔离,无法直接访问对方内存,必须借助内核提供的 IPC 机制(管道、消息队列、共享内存、Socket、信号等)进行通信。
Linux 进程有几个关键属性对 Android 尤为重要:
- PID 与 PPID:进程标识与父进程标识,Android 中 Zygote 是所有应用进程的父进程。
- UID/GID:用户与组标识,Android 用它实现应用级隔离。
- 虚拟地址空间:每个进程独立,是进程隔离的物理基础。
- 进程状态:运行、可中断睡眠、不可中断睡眠、停止、僵尸等。
- 调度优先级:nice 值与实时优先级,Android 在此基础上扩展了 oom_adj。
Android 并没有重新发明进程,而是复用 Linux 进程作为安全与调度的载体,再在其上叠加 Java 运行时、应用组件模型与沙箱策略。
二、Android应用进程:Zygote孵化与组件承载
Android 应用进程并非由用户直接 fork 出来,而是由 Zygote 进程孵化。Zygote 在系统启动时预加载 Android 框架类与资源,随后监听 Socket。当 AMS(ActivityManagerService)需要启动一个应用时,会通过 Socket 通知 Zygote,Zygote 调用 fork() 复制自身,子进程再加载目标应用的 APK 代码,从而快速得到一个已具备运行时环境的进程。
一个应用进程可以承载多个组件:Activity、Service、BroadcastReceiver、ContentProvider。默认情况下,同一应用的所有组件运行在同一进程中,进程名即包名。但开发者可以通过 android:process 属性让组件运行在独立进程,例如:
<service
android:name=".RemoteService"
android:process=":remote" />
<activity
android:name=".PushActivity"
android:process="com.example.push" />
以冒号开头的进程名是应用私有进程,其他应用无法共享;完整包名形式的进程则是全局进程,可被多个应用通过 sharedUserId 共享。多进程会带来内存开销、静态变量失效、单例失效、文件锁竞争等问题,因此应谨慎使用。
三、进程优先级:oom_adj与LMK
移动设备内存有限,Android 不能像桌面系统那样随意换页,而是采用 Low Memory Killer(LMK) 在内存不足时主动杀进程。杀谁、不杀谁,取决于进程的 oom_adj 值。oom_adj 越小,进程越重要,越不容易被杀。
Android 将进程划分为若干优先级层级,核心对应关系如下表:
| 优先级层级 | 典型 oom_adj | 说明 |
|---|---|---|
| 前台进程 Foreground | 0 | 正在交互的 Activity、前台 Service、正在接收广播的 Receiver |
| 可见进程 Visible | 1~2 | 被对话框遮挡但仍可见的 Activity |
| 服务进程 Service | 3~5 | 通过 startService 启动且未绑定前台的 Service |
| 后台进程 Background | 6~8 | 已停止但保留在 LRU 列表中的 Activity 进程 |
| 空进程 Empty | 9~15 | 无任何活跃组件,仅作缓存用途 |
开发者可以通过 adb shell dumpsys meminfo <package> 或 adb shell cat /proc/<pid>/oom_score_adj 观察进程的 oom_adj 值。当系统内存紧张时,LMK 从 oom_adj 最大的进程开始回收,直到内存恢复到安全水位。
startForeground() 并展示通知,否则系统会限制其后台运行,甚至直接杀进程。这是进程优先级策略随版本演进的典型体现。
四、进程隔离与安全沙箱机制
Android 的安全模型核心是应用沙箱。每个应用在安装时被分配一个独立的 Linux UID(如 u0_a123),运行在独立进程中,拥有独立的私有数据目录 /data/data/<package>。不同 UID 的进程无法直接访问彼此的文件与内存,这是内核层面的强制隔离。
沙箱机制由多个层次共同构成:
- UID 隔离:每个应用一个 UID,内核据此判定文件与 IPC 权限。
- 文件系统权限:私有目录权限为 700,仅应用自身可读写。
- SELinux:强制访问控制,限制进程可执行的操作与可访问的资源。
- 权限声明:敏感能力(相机、定位、通讯录)需在 Manifest 声明并在运行时申请。
- 签名校验:应用签名决定其能否升级、共享 UID 或声明特定权限。
当两个应用需要共享数据时,不能直接突破沙箱,而必须通过 Binder、ContentProvider、共享内存等受控通道。这正是 Android 进程通信机制存在的根本原因。
五、案例与误区复盘
考虑一个音乐播放器应用:它有一个前台 Service 负责播放,一个 Activity 展示界面,还有一个后台进程用于下载。播放时,前台 Service 使进程处于前台优先级(oom_adj=0),几乎不会被杀;用户退出界面后,Activity 销毁,进程降为服务进程;下载完成后,若没有其他组件,进程变为空进程,随时可能被 LMK 回收。若开发者把下载任务放在一个独立进程中,且未做进程间状态同步,就会出现“主进程显示下载完成,子进程仍在写文件”的竞态问题。
android:process 可以提升性能。实际上多进程会增加内存占用与 IPC 开销,除非确有必要(如大内存操作、崩溃隔离),否则应保持单进程。常见误区二:认为静态变量可以跨进程共享。每个进程有独立的虚拟机实例,静态变量、单例、线程锁都不共享,必须通过 IPC 或持久化传递。
常见误区三:认为 oom_adj 是固定不变的。它会随组件状态动态调整,开发者应通过
dumpsys 实时观察,而不是凭记忆判断。
总结来看,Android 进程模型可以概括为:以 Linux 进程为底座,以 Zygote 为孵化器,以 oom_adj 为调度依据,以 UID 与 SELinux 为隔离手段,以受控 IPC 为通信桥梁。掌握这五层关系,才能在后续 Binder 与共享内存的工程实践中,准确判断进程边界、优先级变化与安全约束。