Android Framework | 白话Zygote

·  阅读 988

互联网上关于Zygote启动流程的文章已经汗牛充栋,其中不乏深入分析的精品。因此本文无意于从源码层面给出解读,而是希望站在一个更加宏观的视角,写一点通俗易懂的话语。

Zygote,译为“受精卵”,是Android启动过程中第一个Java进程的名称。这个进程的任务只有一个:接收system_server的请求,fork出新的应用进程。

作为系统中的第一个进程,init会根据rc文件执行一系列启动操作。其中有一项便是启动zygote进程。init会fork出一个子进程,然后在其中通过exec加载/system/bin/app_process可执行文件。这一点和其他纯native进程的启动(譬如surfaceflinger)并无二致。

/system/bin/app_process被加载后,进程便会运行可执行文件的main方法。其中主要会做两件事:

  1. 启动运行时。所谓运行时,其实就是Dalvik字节码可以被正确理解并运行的环境,它存在于进程之中。而这种环境通常由两部分组成:一部分负责对象的创建与回收,譬如类的加载器,垃圾回收器等等。另一部分负责程序逻辑的运行,譬如即时编译系统,解释器等等。其实我不太喜欢“创建虚拟机”这样的说法,因为它容易让人产生误解,认为虚拟机是一个独立于进程之外的实体。
  2. Fork出system_server进程。子进程执行完SpecializeCommon函数后进入到com.android.server.SystemServer的main方法中,之后便进入system_server的运行逻辑。

做完这两件事情后,zygote便会将自身挂起,等待来自于system_server的进程启动的请求。

之所以采用fork的方式来启动进程,是因为子进程可以继承父进程的地址空间和运行环境,从而节省子进程的启动时间。因此,问题的关键变成:所有应用进程在启动时刻有哪些操作是共性的?而这些共性的操作都可以放到zygote中去提前处理。

最大的共性操作便是启动运行时。其中会预先加载一些boot class,这些类使用频繁,因此在zygote中提前加载好,可以极大地提升后续应用的启动速度。类的加载过程大体上可以分为两步,第一步是创建类在内存中的实体,第二步是类的初始化,譬如运行static代码块。这也是为什么Android中既需要preloaded-classes,也需要boot.art的原因。即便boot.art和preloaded-classes中包含的是同样的类,但彼此做的事情其实是不同的。boot.art为image文件,其中有很多的类对象(art::mirror::Class对象),一旦加载到内存中,相当于这些类对象已经创建完毕,也即加载的第一步。而preloaded-classes会调用Class.forName去初始化这些类,属于类加载的第二步。

当zygote在fork的时候,它会保持自己为单线程状态。这是因为多线程下的fork很容易在子进程中产生死锁、状态紊乱等一系列问题,而究其根源,是因为即便父进程为多线程,fork之后的子进程也只会有一个线程。这种多对一的转换便会遗漏掉很多同步的信息。因此zygote在fork之前,会关闭HeapTaskDaemon之类的线程。

另外,正是因为fork的这个动作,所以zygote才没有选用Binder作为其跨进程通信的方式(这一点我在19年邮件咨询过Google负责Binder的两位工程师)。因为Binder压根就不支持fork,除非fork后调用exec,开启全新的进程环境。这主要是因为Binder中对象生命周期的管理比较复杂,而如果为了支持fork,那么它的设计将会更加复杂。

当下的Android版本基本都同时支持32位和64位的应用。鉴于此,系统中也存在两个版本的zygote进程,一个为zygote32,作为所有32位应用的父进程;另一个为zygote64,作为所有64位应用的父进程。不过随着Android的发展,32位应用很快会被淘汰出历史舞台。

为了更进一步地提升应用的启动性能,Android 10开始在zygote中引入了一种新的启动机制:USAP。具体的细节可以参考之前的文章

以上,便是我对于zygote的粗浅认识。

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改