Android Zygote:灵感源自生物学的安卓启动机制

我一直对人体的运作方式充满好奇,觉得它就像迄今为止最先进的软件,充满了相互依赖的系统、逻辑和设计模式,所有这些都由大脑协调,大脑可以同时处理并行任务、内存和决策。

软件工程中的许多理念都受到生物学的启发。一个很好的例子就是“合子”(Zygote),这个概念同时存在于自然界和安卓系统中。在生物学中,合子是精子和卵子结合后形成的第一个细胞。它携带完整的DNA蓝图,就像两个配置文件合并成一个独特的设置,包含启动完整人体系统所需的一切。

然后,这个细胞分裂和复制,运行一个生物“构建过程”,最终形成了你和我,一个多线程、自愈、分布式系统。就像软件中的并行线程一样,你的身体同时运行许多任务:心脏跳动、肺呼吸、大脑处理信息,所有这些都在平稳地进行。

安卓的Zygote运作方式类似,但细节上有所不同:它是一个预先初始化的进程,通过“fork”机制创建每个应用,从而节省时间和资源。一个起源,无数克隆,就像生命本身一样。

安卓Zygote的起源:历史与幕后推手

当安卓团队选择Java作为主要编程语言时,他们面临一个挑战:现有的Java虚拟机(VM)并未针对安卓内核(运行在移动设备上的Linux内核)进行优化。由于存在许多针对不同平台的Java VM,每个VM都侧重于不同的优先级,例如垃圾回收或性能,因此团队需要一个专门为移动设备定制的VM。Ben和他的团队开发了Dalvik,一个为安卓设计的虚拟机,运行.dex文件。它优先考虑移动性能因素,包括更低的电池消耗和高效的执行。

然后,他们又面临了一个新的问题:每次应用启动时,都必须从头加载共享类、框架代码、图形驱动程序、库、共享文本资源和其他资源,这会消耗内存、CPU和电池。对于任何软件工程师来说,这都是一个需要解决的性能瓶颈。

Dan Bornstein(谷歌前Virtual Machinist)和他的团队提出了一种受生物学启发的解决方案,并将其命名为Zygote。大部分工作,包括命名(正如Dan与我分享的),都由Mike Fleming完成,我认为这是一个巧妙而精妙的选择。他们的想法是创建一个预加载所有必要组件的单个进程。然后,当系统应用需要打开或用户启动应用时,系统只需“fork”这个预初始化的进程。新创建的应用进程继承所有预加载的资源,从而实现更快的启动时间、减少内存和CPU使用量,并最终降低电池消耗。

Dalvik自Android 5.0起已不再使用,被ART取代,原因很好,这里就不赘述了。但无论是Dalvik还是ART虚拟机,都依赖Zygote来启动应用进程。

安卓Zygote的内部运作机制

(此处原文包含一张图片,描述了Zygote进程生命周期,此处无法显示图片)

安卓系统启动后,Linux内核和其他底层组件加载完毕,init.cpp进程会根据init.rc配置启动Zygote进程。此时,Android Runtime被初始化,Zygote打开一个socket来监听来自系统的fork请求。

Zygote的入口点是一个名为app_process的本地二进制文件,用C++编写。这个二进制文件有几个职责:启动Zygote进程、启动独立的命令行应用,以及根据接收到的参数初始化Android Runtime。由于Zygote本身是用Java实现的,app_process首先加载Android Runtime,然后调用com.android.internal.os.ZygoteInitmain()方法来启动Zygote进程。

以下是系统服务调用此main函数后发生的三个主要阶段:

  1. Hook阶段 在此阶段,ZygoteInit类使用ZygoteHooks提供Zygote初始化开始和结束时的生命周期钩子。这些钩子执行诸如:

    • 暂停线程创建
    • 暂停和恢复JIT编译器
    • 清理特定于fork的资源 等等。 这些钩子确保运行时环境稳定且针对fork进程进行了优化。
  2. 预加载阶段 在这里,ZygoteInit预加载各种类、库和资源,这些资源将由所有fork进程高效共享。关键的预加载函数包括: (此处原文包含一张图片,展示了ZygoteInit中的关键预加载函数,此处无法显示图片)

  3. 预加载后和监听阶段 预加载阶段完成后,ZygoteHooks会重新启用JIT编译器等功能,并让任何暂停的线程继续执行。 然后,Zygote进入一个循环,使用runSelectLoop()等待来自Activity Manager Service的命令。当需要启动新应用时,Zygote接收到请求,创建自身的副本(称为fork),设置应用主线程,并通知上游启动应用主Activity。

几乎你使用的每一个安卓应用,以及重要的系统部分,如System UI和services,都是通过这种方式启动的。这就像一个“复制粘贴”技巧,可以帮助安卓更快地启动应用,并通过共享已经加载的重量级内容来节省内存。这是安卓说:“为什么要做额外的工作,当你只是克隆和放松一下呢?”

Zygote如何启动你的应用

(此处原文包含一张图片,描述了安卓应用启动流程,此处无法显示图片)

我解释了人类Zygote的工作方式;它每次都不是从零开始,只是复制了一套现成的指令(DNA)来培养完整的人。安卓做的事情非常相似。😄

在上面的图中,你可以看到当有人点击你的应用图标时会发生什么。系统启动一系列事件,从Launcher开始,然后通过ActivityTaskManagerService、ActivityStarter、ActivityManagerService、ProcessList,最终到达主角:Zygote。

此时,安卓不会从头开始构建所有东西。相反,Zygote进行fork,基本上为请求的应用制作一个自身的克隆,其中已经打包了预加载的内容。它创建一个新的应用进程,所有设置都已完成并准备就绪。新的应用进程准备好主线程,ActivityManagerService给它一个小小的推动:“好了,伙计,该你了!” 应用的主Activity启动,你的应用的主屏幕就活跃起来了。

这也是为什么当你的应用崩溃/ANR时,你会看到ZygoteInit.main位于堆栈跟踪的底部,仅仅是因为它是你的应用进程的“母亲”。 🧬 👶 📱

(此处原文包含一个崩溃堆栈跟踪的示例,此处无法显示)

结论:

在这篇文章中,我们探讨了早期的安卓团队如何从生物学中汲取灵感,解决一个大问题:为每个应用从头加载所有内容。他们巧妙的想法是使用一个共享进程(称为Zygote),该进程已经加载并准备就绪,以便应用可以更快地启动。我们看到了这在AOSP代码中是如何工作的,并了解了名称的由来。

我分享的大部分内容都来自于深入研究AOSP、书籍以及与一些在安卓Dalvik和ART早期就参与其中的人士的交流。

如果你喜欢我所写的内容,并且你希望在一个关心环境下的开放、有趣的项目中工作,请查看Delivery Hero的开放职位列表:从后端到前端,以及介于两者之间的一切。我们很乐意欢迎你加入我们,开启一段精彩的旅程。另外,如果你像我一样热爱安卓,欢迎在Medium上关注我,查看我的GitHub,或在LinkedIn上联系我。我很高兴认识充满好奇心的工程师。