0%

Android Zygote

为什么要用Zygote孵化一个进程?直接创建一个进程会有什么问题?

Zygote 作为一个孵化器,可以提前加载一些资源,这样 fork() 时基于 Copy-On-Write 机制创建的其他进程就能直接使用这些资源,而不用重新加载。

比如 system_server 就可以直接使用 Zygote 中的 JNI 函数、共享库、常用的类、以及主题资源。

应用在启动的时候需要做很多准备工作,包括启动虚拟机,加载各类系统资源等等,这些都是非常耗时的,如果能在zygote里就给这些必要的初始化工作做好,子进程在fork的时候就能直接共享,那么这样的话效率就会非常高。

这个就是zygote存在的价值,这一点呢SystemServer是替代不了的,主要是因为SystemServer里跑了一堆系统服务,这些是不能继承到应用进程的。

而且我们应用进程在启动的时候,内存空间除了必要的资源外,最好是干干净净的,不要继承一堆乱七八糟的东西。

所以呢,不如给SystemServer和应用进程里都要用到的资源抽出来单独放在一个进程里,也就是这的zygote进程,然后zygote进程再分别孵化出SystemServer进程和应用进程。

孵化出来之后,SystemServer进程和应用进程就可以各干各的事了。

Zygote预加载了那些资源?

Zygote进程,是由init进程通过解析init.rc文件后fork生成的,Zygote进程主要包含:

  • 加载ZygoteInit类,注册Zygote Socket服务端套接字
  • 加载虚拟机
  • 提前加载类preloadClasses
  • 提前加载资源preloadResouces

ZygoteInit.preload()

preloadClasses()用来预加载类
会读取配置文件/system/etc/preloaded-classes,部分内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
com.android.internal.util.ParseUtils
com.android.internal.util.Preconditions
dalvik.system.CloseGuard
dalvik.system.DalvikLogHandler
java.io.ByteArrayInputStream
java.io.ByteArrayOutputStream
javax.crypto.AEADBadTagException
javax.crypto.BadPaddingException
org.ccil.cowan.tagsoup.ElementType
org.json.JSONArray
org.json.JSONException
org.w3c.dom.Element
org.w3c.dom.Node
sun.misc.LRUCache
sun.misc.Unsafe

都是android sdk的类和java包类。

加载类的方式就是直接Class.forName()
// Load and explicitly initialize the given class. Use
// Class.forName(String, boolean, ClassLoader) to avoid repeated stack lookups
// (to derive the caller’s class-loader). Use true to force initialization, and
// null for the boot classpath class-loader (could as well cache the
// class-loader of this class in a variable).
Class.forName(line, true, null);

preloadResources()方法加载资源,加载过程就是调用一些Resources类的obtainTypedArray,然后遍历每项资源,调用Resources.getDrawable(),Resources的内部应该有缓存。

也就是framework.jar和framework-res.jar里的东西。

每一个app进程都应该公用这些东西,不应该重新加载一遍。

/system/framework/下的各种jar是什么时候加载的?在哪里指定加载的?

在init.rc中export BOOTCLASSPATH

Zygote进程启动流程

Zygote 进程和SystemServer进程通信为什么不采用 Binder 而采用Socket?

原因是因为fork只能拷贝当前线程,不支持多线程的fork。

如果zygote使用binder的多线程模型与system_server进程进行通讯的话,fork()出的App进程的binder通讯没法用,那么只能再使用exec()启动一个新进程。

但是exec()启动的新进程不再包含zygote进程的信息,那这样的就失去了fork的作用了,fork的原理就是copy-on-write机制,zygote进程中已经启动了虚拟机、进行资源和类的预加载以及各种初始化操作,App进程用时拷贝即可。

所以最终zygote采用的方案就是socket + epoll,然后fork出子进程后再在子进程中启动binder线程池。

fork为什么不允许多线程?

fork() 时只会把调用线程拷贝到子进程、其他线程都会立即停止,那如果一个线程在 fork() 前占用了某个互斥量,fork() 后被立即停止,这个互斥量就得不到释放,再去请求该互斥量就会发生死锁了。

我们知道通过fork创建的一个子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份拷贝,包括文本、数据和bss段、堆以及用户栈等。子进程还获得与父进程任何打开文件描述符相同的拷贝,这就意味着子进程可以读写父进程中任何打开的文件,父进程和子进程之间最大的区别在于它们有着不同的PID。

但是有一点需要注意的是,在Linux中,fork的时候只复制当前线程到子进程,在fork(2)-Linux Man Page中有着这样一段相关的描述:

The child process is created with a single thread–the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.

也就是说除了调用fork的线程外,其他线程在子进程中“蒸发”了。

这就是多线程中fork所带来的一切问题的根源所在了。