Android App 启动流程:Launcher、AMS、Zygote 与 ActivityThread

梳理 Android App 从点击图标到首个 Activity 显示的流程:Launcher、AMS、Zygote、ActivityThread、Application 与 Activity 生命周期。

从点击图标到页面显示

App 冷启动可以拆成三个关键角色:Launcher 发起请求,System Server 中的 AMS 负责调度,Zygote 负责 fork 新进程。

核心结论

  1. Launcher 不能直接创建目标 App 进程,它通过 Binder 请求 AMS 启动 Activity。
  2. AMS 如果发现目标进程不存在,会通过 Socket 请求 Zygote fork 新进程。
  3. Zygote 使用 fork 和 Copy-on-Write 复用预加载类与资源。
  4. 新进程入口是 ActivityThread.main(),这里会启动主线程 Looper。
  5. AMS 通过 IApplicationThread 通知新进程绑定 Application 并启动 Activity。

第一阶段:请求与孵化(The Request & The Fork)

Launcher 自己不能创建目标 App 进程,它必须把启动请求交给 AMS。

1. Launcher -> AMS (Binder 通信)

  • 动作: 当你点击桌面图标,Launcher 进程调用startActivity
  • 底层: Launcher(Client)通过 Binder 告诉 AMS(Server):“我要启动某某 App 的 MainActivity”。
  • 知识点:这里发生了一次 Binder IPC。

2. AMS的决策

  • AMS 检查这个 App 的进程是否已经存在。
  • 如果存在(热启动):直接通过 Binder 通知该进程onResume,速度很快。
  • 如果不存在(冷启动):AMS 必须创建一个新进程。

3. AMS -> Zygote(Socket通信 – 高频)

  • 动作:AMS 发现需要新进程,它不是自己去创建,而是通知 Zygote 进程
  • 底层:AMS 通过LocalSocket发送请求给 Zygote。
  • 必须知道:为什么这里不用 Binder 而用 Socket
    • 死锁风险:Zygote 的主要工作是fork()。在 Linux 中,fork()只会拷贝当前线程,而不会拷贝其他线程。如果 Zygote 使用 Binder(它是多线程的),且fork()时某个 Binder 锁被其他线程持有,新进程里这个锁就永远处于被锁住的 状态(不会被释放?因为持有线程的锁没有被拷贝过来),导致新进程一启动就死锁
    • Socket 优势:Socket 是单线程、顺序执行的,对于fork这种敏感操作更安全。

4. Zygote -> 新进程(Fork)

  • 动作:Zygote 收到请求,执行fork()系统调用,复制自身,生成一个新的 App 进程。
  • 底层:利用**Copy-on-Write(写时复制)**技术。新进程共享 Zygote 的资源库(系统类、资源),只有当新进程需要修改数据时,才会真正拷贝物理内存。这就是 Android 应用启动快,省内存的秘诀。

第二阶段:初始化与绑定(Init & Attach)

新进程刚出生时是一张白纸,它不知道自己是谁,也不知道要干嘛。

5. 新进程启动(ActivityThread.main)

  • 动作: 新进程开始执行入口函数ActivityThread.main()
  • 底层:这里会初始化我们熟悉的Looper.prepareMainLooper()Looper.loop()主线程的消息循环从此开始运转

6. 新进程 – AMS(Binder 通信)

  • 动作:新进程(现在它是 Client)通过 Binder 告诉 AMS(Server):“大佬,我启动好了,请把 Application 信息发给我。”
  • 方法名: attachApplication
  • 知识点:这是新进程主动发起的第一次 Binder 调用。

第三阶段:生命周期回调(LifeCycle)

AMS 确认“人”到了,开始派活。

7. AMS -> 新进程(Binder通信)

  • 动作: AMS 通过 Binder 接口(IApplicationThread)发回指令。
  • 指令 1:bindApplication – 触发Application.onCreate()
  • 指令 2:scheduleLaunchActivity – 触发 Activity.onCreate()

8. 主线程处理(Handler)

  • 动作: 新进程的 Binder 线程池收到 AMS 指令后,通过Handler把消息抛给主线程(H类)。
  • 执行: 主线程 Looper 收到消息后,执行handleBindApplicationhandleLaunchActivity
  • 结果:在屏幕上终于显示出了界面。

核心图谱:Android 进程启动三部曲

我们要搞清楚三个角色的出场顺序:Init 进程 -> Zygote进程 -> System Server 进程。

第一阶段:Init 进程(PID = 1)

  • 角色: Linux 系统的第一个用户级进程,所有进程的老祖宗。
  • 动作:
    1. 电源键按下,Bootloader 引导 kernel 启动。
    2. Kernel 启动完毕后,查找并执行/init文件。
    3. Init 进程读取init.rc配置文件。
    4. 关键指令: 在配置文件里写着一行service zygote ...,Init 也就是在这里把 Zygote启动了起来。

第二阶段:Zygote 进程

  • 角色:Android Java 世界的孵化器。所有的 App 进程(包括 System Server)都是它的子孙。
  • 动作(ZygoteInit.main):
    1. 启动虚拟机(ART/Dalvik):这是 Java 代码运行的基础。
    2. 预加载资源(Preload):(面试必考)Zygote 会把常用的系统类(String, Activity等)和资源(drawable, styles)加载到自己的内存里。
      • 为什么要这样做?因为后面 fork 出的新进程会继承这些内存(Copy-on-Write)。大家共享这些资源,既省内存,启动又快。
    3. 启动 System Server:Zygote 会 fork 出 System Server 进程。
    4. 建立 Socket 服务端: 任务完成,Zygote 建立一个LocalSocket服务端,进入死循环,等待 System Server 发号施令。

第三阶段:System Server 进程

  • 角色: Android 系统的“CEO”。它虽然是 Zygote 生成的,但它掌管着 Zygote 的接单权。
  • 动作(SystemServer.main):
    1. 启动 Binder 线程池:(关键差异) Zygote 为了防死锁不用 Binder,但 System Server 是大管家,必须处理海量并发请求,所以它第一件事就是初始化 Binder。
    2. 启动系统服务:它一口气启动了 80+ 个服务,包括我们熟悉的:
      • AMS (Activity Manager Service) : 统管四大组件。
      • WMS(Window Manager Service):统管窗口。
      • PMS (Package Manager Service):统管安装包。
    3. 进入 Looper 循环:这里的 Looper 就在等着处理各种 Binder 也就是 App 发来的请求了。

夺命连环问

1. 为什么System Server 要被Zygote Fork出来,而不是有Init直接启动?

这个问题考察对**Copy-on-Write(COW)**的理解。

  • 回答要点:
    • 资源共享:System Server 本质上也是一个 Java 进程,它也需要运行环境和系统类。
    • 继承红利:如果 Init 直接启动,System Server 就得自己重新加载一遍虚拟机和资源,既慢有浪费内存。让 Zygote fork 它,它就能直接继承 Zygote 预加载好的所有资源(类、图片、各种 drawable),落地即满级。
2. Zygote 和System Server是怎么配合工作的?

这个问题考察你对 IPC 架构的理解。

  • 回答要点:
    • 父子关系:Zygote fork 了 System Server。
    • C/S 关系(Socket):
      • System Server(里的 AMS)充当 Client。
      • Zygote 充当 Socket Server
      • 当 AMS 需要启动新 App 时,它通过 Socket 发消息给 Zygote:“给我 fork 个新进程”。
    • C/S 关系(Binder):
      • App 启动后(Client),会通过 Binder 找 AMS 注册自己。
3. 如果System Server挂了(Crash)会发生什么?

这个问题考察你对 Android 系统稳定性的认知

  • 回答要点:
    • 手机重启(Soft Reboot)。
    • Zygote 会监听他的子进程。如果 System Server 挂了,Zygote 会"自杀"(因为大管家没了,系统没法运行了)。
    • Init 进程发现 Zygote 挂了,会重启 Zygote。
    • Zygote 重启后,又会再次启动 System Server。
    • 这就是为什么有时候手机卡死黑屏后,会显示开机动画重新进入系统。
4. 为什么zygote不直接当System Server呢

一句话就是 各司其职。具体来说,是因为 内存洁癖生死隔离并发死锁权限安全这四大硬性约束。

我们来逐一拆解:

1. 内存洁癖:Zygote必须是"纯净的母体"

这是最核心的原因。

  • Zygote的角色: 它是一个模板(Template) 。他的内存里应该只包含所有 App 公用的、只读的基础资源(比如String类、View类、drawable资源)。
  • System Server 的角色:它是一个管理员。它的内存里塞满了 动态的、脏的运行时数据(比如当前运行了哪些 App、窗口的位置、电池电量、用户的锁屏密码等)。

如果 Zygote 直接当 System Server:Zygote 的内存里就会包含上述的脏数据。当 Zygote fork 一个新的 App(比如微信)时,根据Copy-on-Write机制,微信会继承 Zygote 的所有内存数据

  • 后果:微信一启动,内存里就莫名其妙多了"当前所有 App 的列表"、“电池电量记录”等它根本不需要、甚至不该知道的数据。这不仅浪费内存,还可能导致数据泄露。

所以:Zygote 必须保持“纯洁”和“静态”,只包含通用的代码和资源。一旦 fork 出 System Server,System Server 就可以尽情地在自己的进程里搞脏自己的内存,而不会影响后续孵化出来的 App。

2. 生死隔离: Zygote不能死
  • Zygote 的使命:它是 Android 世界的女娲。只要它活着,App 挂了可以重开,System Server 挂了可以重启。
  • System Server 的风险:它运行着 80+ 个系统服务,逻辑极其复杂,代码量巨大,还要处理各种第三方 App 的奇怪请求。它崩溃的概率远高于 Zygote。

如果 Zygote 直接当 System Server:一旦 AMS 或 WMS 因为某个 Bug 崩溃了,整个 Zygote 进程就会挂掉。

  • 后果: Android 系统彻底死透了。因为孵化器没了,没人能重建系统,也没人能孵化新 App,只能等待 Linux 内核(Init 进程)重启手机。

现在的架构:System Server 挂了 -> Zygote 还在 -> Zygote 监听到子进程死亡 -> Zygote 重启 System Server -> 手机"软重启"(Soft reboot,只闪一下开机动画,不用重新引导内核)。

3. 并发死锁:Binder与Fork的天生互斥

我们在前面提到过,Zygote 为什么用 Socket 而不用 Binder?因为 Binder 是多线程的,而fork()在多线程环境下即易死锁。

  • System Server: 必须高度依赖 Binder(它是系统的大管家,要处理成千上万的并发请求)。所以 System Server 里有密密麻麻的 Binder 线程池。
  • Zygote :必须保持单线程(或者极简的线程模型)、才能安全地执行fork()

如果 Zygote 直接当 System Server:它既要运行 Binder 线程池来处理系统服务,又要执行fork()来孵化 App。

  • 后果:当 zygote 正在处理一个 Binder 请求(锁住了某个资源)时,突然来了一个孵化请求执行fork()。新生成的 App 进程里,那个"锁"是被锁住的,但持有锁的线程没被拷贝过来 – 死锁发生了,新 App 永远卡死在启动界面
4. 权限安全:Root vs System
  • Zygote: 必须拥有 Root 权限。因为它要操作底层资源,要setuid(给新 App 分配独立的用户 ID),这都是特权操作。
  • System Server: 只需要 System 权限(uid=1000)。它虽然权力大,但不能拥有 Root 权限,以防被黑客利用攻破整个系统内核。

现在的架构:Zygote(Root) -> fork() -> 新进程 -> 降权(Drop Permission) -> 变成了 System Server (System User)或者 App(App User)。这样最安全,权限最小化。

5. 为什么Binder 被锁阻塞的线程拷贝过来就永远是锁住的呢?

简单的答案是:因为fork()只有复制当前执行fork()的那个线程,而不会复制其他线程。但是,它却复制了所有线程持有锁的状态。

假设 Zygote 进程里有两个线程:

  1. 线程 A(Binder 线程):正在准备处理一个系统请求,它拿了一把锁(Mutex),准备写日志。
  2. 线程 B(主线程):准备执行fork()来孵化新进程(比如启动微信)。
第一步:Zygote正常运行
  • 线程 A 运行到一半,执行了lock.acquire()(加锁)。
  • 此时,锁(Mutex)的状态在内存里变成了"Locked"。
  • 线程 A 还没来的及释放锁,CPU 时间片到了,切到了线程 B。
第二步:执行Fork
  • 线程 B 执行 fork
  • 关键点:Linux 的 fork()规定,只复制调用 fork 的那个线程(也就是线程 B)。
  • 后果:新生成的进程(微信进程)里,只有线程 B 的克隆体,没有线程 A
第三步:遗产继承(灾难开始)
  • 虽然线程 A 没过来,但是 Zygote 进程里的所有内存数据都被复制过来了(通过 Copy-on-Write)。
  • 重点:那把锁(Mutex)是内存里的一个对象。在 Zygote 里,它是"Locked"状态,所以,在新进程里,这把锁也依然是 Locked 状态
第四步:新进程启动
  • 新进程(微信)开始初始化。
  • 微信的初始化代码里,也需要写日志(或者使用 Binder)。
  • 微信的主线程尝试执行lock.accquire()
第五步:死锁(DeadLock)
  • 微信的主线程发现:咦?这把锁已经被锁住了
  • 于是微信主线程挂起,等待锁被释放
  • 但是!真正持有这把锁的人 – Zygote 里的线程 A – 根本没有被拷贝到微信进程里来
  • 结局:微信进程流永远没有任何人能去执行lock.release()。这把锁成了“幽灵锁”,微信进程一出生就卡死在初始化阶段,永远等待一个不存在的线程来解锁。

什么是Copy-on-Write

这是操作系统偷懒的内存管理策略。

简单一句话总结:只有在真正修改数据时,才去复制一份;如果只是读取,那就大家共用一份

1. 核心原理:能不复制,就不复制

为了彻底理解,我们还是用 Zygote 和新 App 的例子。

假设 Zygote 进程内存里有一张 100MB 的资源地图(里面有 String 类,Drawable 图片,系统主题等)。

第一阶段:Fork发生时(假装复制)

当 Zygote 执行fork()孵化新App 时:

  • 操作系统不做的事:它不会傻乎乎的把这 100MB 物理内存 Copy 一份给新 App。那太慢了
  • 操作系统做的事:他只是把 Zygote 的“页表”(Page Table, 也就是内存地址映射关系)复制了一份给新 App。
  • 结果: Zygote 和新 App 的虚拟地址虽然是独立的,但它们指向的物理内存是完全同一块
  • 标记:操作系统会把这块共享的内存区域标记为只读(Read-Only)。
第二阶段:大家只读不写(和平共处)
  • 只要新 App 和 Zygote 都只是读取这些数据(比如读取一个 String,或者加载一张系统图标),它们就一直共用这块物理内存。
  • 收益:内存占用极低,因为没有产生新的副本。
第三阶段:有人要搞事了(触发写时复制)
  • 动作:突然,新 App 想要修改某个变量(比如把一个静态变量sConfig ="default" 改为custom)。
  • 冲突: 也就是写操作。
  • 异常:因为这块内存被标记为“只读”,CPU 会立即抛出一个** 缺页异常(Page Fault)**,告诉操作系统有人想要只读数据
  • 处理:操作系统介入。它会说:好吧,既然你要改,那这页数据就不能共享了。 1. 操作系统把 **这一页(通常是 4KB)**数据复制一份 ,生成一个新的物理内存页。 1. 把新 App 的页表指向这个 新的物理页 1. 把新页的权限改为可读写。
  • 结果:现在,新 App 可以随意修改这份新的数据了,而 Zygote 手里的那份依然是旧的。它们从此在这一页数据上分道扬镳。