1. 前面Android(java)学习笔记159提到Dalvik虚拟机启动初始化过程,就下来就是启动zygote进程:
zygote进程是所有APK应用进程的父进程:每当执行一个Android应用程序,Zygote就会孵化一个子线程去执行该应用程序(系统内部执行dvz指令完成的)
特别注意:系统提供了一个app_process进程,它会自动启动ZygoteInit.java和SystemServer.java这两个类,app_process进程本质上是使用dalvikvm启动ZygoteInit.java。并且启动后加载Framework中大部分类和资源。
2. zygote进程启动流程:
(1)在init.rc中配置zygote启动参数:
init.rc存在于设备的根目录下,读者可以使用adb pull /init.rc ~/Desktop命令取出该文件,文件中和zygote相关的配置信息如下:
-
service zygote /system/bin/app_process -Xzygote
/system/bin --zygote --start-system-server - socket zygote stream 666
- onrestart write /sys/android_power/request_state wake
- onrestart write /sys/power/state on
- onrestart restart media
- onrestart restart netd
首先第一行中使用service指令告诉操作系统将zygote程序加入到系统服务中,service的语法如下:
- service service_name 可执行程序的路径 可执行程序自身所需的参数列表
service service_name 可执行程序的路径 可执行程序自身所需的参数列表
此处的服务被定义为zygote,理论上讲该服务的名称可以是任意的。可执行程序的路径正是/system/bin/app_process,也就是前面所讲的app_process,参数一共包含四个,分别如下:
-Xzygote,该参数将作为虚拟机启动时所需要的参数,是在AndroidRuntime.cpp类的startVm()函数中调用JNI_CreateJavaVM()时被使用的。
/system/bin,代表虚拟机程序所在目录,因为app_process完全可以不和虚拟机在同一个目录,而在app_process内部的AndroidRuntime类内部需要知道虚拟机所在的目录。
--zygote,指明以ZygoteInit类作为虚拟机执行的入口,如果没有--zygote参数,则需要明确指定需要执行的类名。
--start-system-server,仅在指定--zygote参数时才有效,意思是告知ZygoteInit启动完毕后孵化出第一个进程SystemServer。
接下来的配置命令socket用于指定该服务所使用到的socket,后面的参数依次是名称、类型、端口地址。
onrestart命令指定该服务重启的条件,即当满足这些条件后,zygote服务就需要重启,这些条件一般是一些系统异常条件。
(2)启动Socket服务端口
当zygote服务从app_process开始启动后,会启动一个Dalvik虚拟机,而虚拟机执行的第一个Java类就是ZygoteInit.java,因此接下来的过程就从ZygoteInit类的main()函数开始说起。main()函数中做的第一个重要工作就是启动一个Socket服务端口,该Socket端口用于接收启动新进程的命令。
(3)加载preload-classes
在ZygoteInit类的main()函数中,创建完Socket服务端后还不能立即孵化新的进程,因为这个"卵"中还没有必须"核酸",这个"核酸"就是指预装的Framework大部分类及资源。预装的类列表是在framework.jar中的一个文本文件列表,名称为preload-classes,该列表的原始定义在frameworks/base/preload-classes文本文件中,而该文件又是通过frameworks/base/tools/preload/
(4)加载preload-resources
preload-resources是在frameworks/base/core/res/res/values/arrays.xml中被定义的,包含两类资源,一类是drawable资源,另一类是color资源,如以下代码所示:
- <array name="preloaded_drawables">
- <item>@drawable/sym_def_app_icon</item>
- ... ...
- </array>
- <array name="preloaded_color_state_lists">
- <item>@color/hint_foreground_dark</item>
- ... ...
- </array>
而加载这些资源是在preloadResources()函数中完成的,该函数中分别调用preloadDrawables()和preloadColorStateLists()加载这两类资源。加载的原理很简单,就是把这些资源读出来放到一个全局变量中,只要该类对象不被销毁,这些全局变量就会一直保存。
保存Drawable资源的全局变量是mResources,该变量的类型是Resources类,由于该类内部会保存一个Drawable资源列表,因此,实际上缓存这些Drawable资源是在Resources内部;保存Color资源的全局变量也是mResources,同样,Resources类内部也有一个Color资源的列表。
(5)使用folk启动新进程
folk是Linux系统的一个系统调用,其作用是复制当前进程,产生一个新的进程。新进程将拥有和原始进程完全相同的进程信息,除了进程id不同。进程信息包括该进程所打开的文件描述符列表、所分配的内存等。当新进程被创建后,两个进程将共享已经分配的内存空间,直到其中一个需要向内存中写入数据时,操作系统才负责复制一份目标地址空间,并将要写的数据写入到新的地址中,这就是所谓的copy-on-write机制,即"仅当写的时候才复制",这种机制可以最大限度地在多个进程*享物理内存。
第一次接触folk的读者可能觉得奇怪,为什么要复制进程呢?在大家熟悉的Windows操作系统中,一个应用程序一般对应一个进程,如果说要复制进程,可能的结果就是从计算器程序复制出一个Office程序,这听起来似乎很不合理。要立即复制进程就需要首先了解进程的启动过程。
在所有的操作系统中,都存在一个程序装载器,程序装载器一般会作为操作系统的一部分,并由所谓的Shell程序调用。当内核启动后,Shell程序会首先启动。常见的Shell程序包含两大类,一类是命令行界面,另一类是窗口界面,Windows系统中Shell程序就是桌面程序,Ubuntu系统中的Shell程序就是GNOME桌面程序。Shell程序启动后,用户可以双击桌面图标启动指定的应用程序,而在操作系统内部,启动新的进程包含三个过程。
第一个过程,内核创建一个进程数据结构,用于表示将要启动的进程。
第二个过程,内核调用程序装载器函数,从指定的程序文件读取程序代码,并将这些程序代码装载到预先设定的内存地址。
第三个过程,装载完毕后,内核将程序指针指向到目标程序地址的入口处开始执行指定的进程。当然,实际的过程会考虑更多的细节,不过大致思路就是这么简单。
在一般情况下,没有必要复制进程,而是按照以上三个过程创建新进程,但当满足以下条件时,则建议使用复制进程:即两个进程*享了大量的程序。
举个例子,去澳大利亚看袋鼠和去澳大利亚看考拉,这是两个进程,但完成这两个进程的大多数任务都是相同的,即先订机票,然后带照相机,再坐地铁到首都机场,最后再坐14个小时的飞机到澳大利亚,到了之后唯一不同就是看考拉和袋鼠。为了更有效地完成这两个任务,可以先雇佣一个精灵进程,让它订机票、带相机、坐地铁、乘飞机,一直到澳大利亚后,从这个精灵进程中复制出两个进程,一个去看考拉,另一个去看袋鼠。如果你愿意,还可以去悉尼歌剧院,这就是进程的复制,其好处是节省了大量共享的内存。
由于folk()函数是Linux的系统调用,Android中的Java层仅仅是对该调用进行了JNI封装而已,因此,接下来以一段C代码来介绍folk()函数的使用,以便大家对该函数有更具体的认识:
/**
*FileName: MyFolk.c
*/
#include <sys/types.h>
#include <unistd.h>
int main(){
pid_t pid;
printf("pid = %d, Take camera, by subway, take air! \n", getpid());
pid = folk();
if(pid > ){
printf("pid=%d, 我是精灵! \n", getpid());
pid = folk();
if(!pid) printf("pid=%d, 去看考拉! \n", getpid());
}
else if (!pid) printf("pid=%d, 去看袋鼠! \n", getpid());
else if (pid == -) perror("folk");
getchar();
}
以上代码的执行结果如下:
$ ./MyFolk.bin
pid = , Take camera, by subway, take air!
pid=, 我是精灵!
pid=, 去看袋鼠!
pid=, 去看考拉!
folk()函数的返回值与普通函数调用完全不同。当返回值大于0时,代表的是父进程;当等于0时,代表的是被复制的进程。换句话说,父进程和子进程的代码都在该C文件中,只是不同的进程执行不同的代码,而进程是靠folk()的返回值进行区分的。
由以上执行结果可以看出,第一次调用folk()时复制了一个"看袋鼠"进程,然后在父进程中再次调用folk()复制了"看考拉"的进程,三者都有各自不同的进程id。
zygote进程就是本例中的"精灵进程",那些"拿相机、坐地铁、乘飞机"的操作就是zygote进程中加载的preload-classes类具备的功能。
ZygoteInit.java中复制新进程是通过在runSelectLoopMode()函数中调用ZygoteConnection类的runOnce()函数完成的,而该函数中则调用了以下代码用于复制一个新的进程。
forkAndSpecialize()函数是一个native函数,其内部的执行原理和上面的C代码类似。
当新进程被创建好后,还需要做一些"善后"工作。因为当zygote复制新进程时,已经创建了一个Socket服务端,而这个服务端是不应该被新进程使用的,否则系统中会有多个进程接收Socket客户端的命令。因此,新进程被创建好后,首先需要在新进程中关闭该Socket服务端,并调用新进程中指定的Class文件的main()函数作为新进程的入口点。而这些正是在调用forkAndSpecialize()函数后根据返回值pid完成的,如以下代码所示:
pid等于0时,代表的是子进程,handleChildProc()函数中的关键代码如下,首先是关于Socket服务端。
接着从指定Class文件的main()函数处开始执行,如以下代码所示:
至此,新进程就完全脱离了zygote进程的孵化过程,成为一个真正的应用进程。
3. 上面的说明可能不太形象,下面感性的说明一下,如下:
(1)我们来看看每个android进程如何产生的: