为什么要用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 | com.android.internal.util.ParseUtils |
都是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所带来的一切问题的根源所在了。