码迷,mamicode.com
首页 > 移动开发 > 详细

Android(java)学习笔记160:Framework运行环境之启动Zygote

时间:2015-08-15 11:42:45      阅读:289      评论:0      收藏:0      [点我收藏+]

标签:

前面Android(java)学习笔记159提到Dalvik虚拟机启动初始化过程,就下来就是启动zygote进程:

zygote进程所有APK应用进程的父进程每当执行一个Android应用程序,Zygote就会孵化一个子线程去执行该应用程序(系统内部执行dvz指令完成的)

 Å特别注意:系统提供了一个app_process进程,它会自动启动ZygoteInit.java和SystemServer.java这两个类,app_process进程本质上是使用dalvikvm启动ZygoteInit.java。并且启动后加载Framework中大部分类和资源。

zygote进程启动流程:

(1)在init.rc中配置zygote启动参数:

init.rc存在于设备的根目录下,读者可以使用adb pull /init.rc ~/Desktop命令取出该文件,文件中和zygote相关的配置信息如下:

  1. service zygote /system/bin/app_process -Xzygote 
    /system/bin --zygote --start-system-server  
  2.     socket zygote stream 666  
  3.     onrestart write /sys/android_power/request_state wake  
  4.     onrestart write /sys/power/state on  
  5.     onrestart restart media  
  6.     onrestart restart netd  

首先第一行中使用service指令告诉操作系统将zygote程序加入到系统服务中,service的语法如下:

  1. 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资源,如以下代码所示:

  1. <array name="preloaded_drawables"
  2.        <item>@drawable/sym_def_app_icon</item
  3. ... ...  
  4.    </array
  5.  
  6.    <array name="preloaded_color_state_lists"
  7.        <item>@color/hint_foreground_dark</item
  8.         ... ...  
  9.    </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 > 0){  
        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 == -1) perror("folk");  
    getchar();  
}  

以上代码的执行结果如下:

$ ./MyFolk.bin  
pid = 3927, Take camera, by subway, take air!   
pid=3927, 我是精灵!   
pid=3929, 去看袋鼠!   
pid=3930, 去看考拉!  

      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进程的孵化过程,成为一个真正的应用进程。

Android(java)学习笔记160:Framework运行环境之启动Zygote

标签:

原文地址:http://www.cnblogs.com/hebao0514/p/4731938.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!