Dalvik虚拟机进程和线程的创建过程分析

来源:互联网 发布:价格标签打印软件 编辑:程序博客网 时间:2024/05/22 06:12

 

 

Dalvik虚拟机进程和线程的创建过程分析: 
  Dalvik虚拟机所创建的进程和线程与其宿主Linux内核的进程和线程有什么关系?Dalvik虚拟机除了可以执行Java代码之外,还可以执行Native代码,也就是C/C++函数。这些C/C++函数在执行的过程中,又可以通过本地操作系统提供的系统调用来创建本地操作系统进程或者线程,也就是Linux进程和线程。如果在Native代码中创建出来的进程又加载有Dalvik虚拟机,那么它实际上又可以看作是一个Dalvik虚拟机进程。另一方面,如果在Native代码中创建出来的线程能够执行Java代码,那么它实际上又可以看作是一个Dalvik虚拟机线程。 
  为了理清它们之间的关系,我们将按照以下四个情景来组织本文:1.Dalvik虚拟机进程的创建过程;2.Dalvik虚拟机线程的创建过程;3.只执行C/C++代码的Native线程的创建过程;4. 能同时执行C/C++代码和Java代码的Native线程的创建过程。对于上述进程和线程,Android系统都分别提供有接口来创建:1.Dalvik虚拟机进程可以通过android.os.Process类的静态成员函数start来创建;2.Dalvik虚拟机线程可以通过java.lang.Thread类的成员函数start来创建;3.只执行C/C++代码的Native线程可以通过C++Thread的成员函数run来创建;4.能同时执行C/C++代码和Java代码的Native线程也可以通过C++Thread的成员函数run来创建; 
  Dalvik虚拟机进程实际上就是通常我们所说的Android应用程序进程。Android应用程序进程是由ActivityManagerService服务通过android.os.Process类的静态成员函数start来请求Zygote进程创建的,而Zyogte进程最终又是通过dalvik.system.Zygote类的静态成员函数forkAndSpecialize来创建该Android应用程序进程的。 
  只有Zygote进程才有权限创建System进程和Android应用程序进程。 
  当一个进程使用系统调用fork来创建一个新进程的时候,前者就称为父进程,后者就称为子进程。这时候父进程和子进程共享的地址空间是一样的,但是只要某个地址被父进程或者子进程进行写入操作的时候,这块被写入的地址空间才会在父进程和子进程之间独立开来,这种机制就称为COWcopy on write)。 
  由Zygote进程创建出来的System进程和Android应用程序进程实际上是共享了很多东西,而且只要这些东西都是只读的时候,它们就会一直被共享着。 
 
  Dalvik虚拟机的启动过程分析: 
  在Android系统中,应用程序进程都是由Zygote进程孵化出来的,而Zygote进程是由Init进程启动的。Zygote进程在启动时会创建一个Dalvik虚拟机实例,每当它孵化一个新的应用程序进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面去,从而使得每一个应用程序进程都有一个独立的Dalvik虚拟机实例。 
  Zygote进程在启动的过程中,除了会创建一个Dalvik虚拟机实例之外,还会将Java运行时库加载到进程中来,以及注册一些Android核心类的JNI方法来前面创建的Dalvik虚拟机实例中去。注意,一个应用程序进程被Zygote进程孵化出来的时候,不仅会获得Zygote进程中的Dalvik虚拟机实例拷贝,还会与Zygote一起共享Java运行时库,这完全得益于Linux内核的进程创建机制(fork)。这种Zygote孵化机制的优点是不仅可以快速地启动一个应用程序进程,还可以节省整体的内存消耗,缺点是会影响开机速度,毕竟Zygote是在开机过程中启动的。不过,总体来说,是利大于弊的,毕竟整个系统只有一个Zygote进程,而可能有无数个应用程序进程,而且我们不会经常去关闭手机,大多数情况下只是让它进入休眠状态。 
  Zygote进程在启动的过程中,会调用到AndroidRuntime类的成员函数start,接下来我们就这个函数开始分析Dalvik虚拟机启动相关的过程,可以分为8个步骤: 1.AndroidRuntime.start  2.AndroidRuntime.startVm 3.JNI_CreateJavaVM  4.dvmCreateJNIEnv 5.dvmStartup 6.dvmInitZygote  7.AndroidRuntime.startReg 8.androidSetCreateThreadFunc 
 
  Dalvik虚拟机JNI方法的注册过程分析: 
  Dalvik虚拟机JNI方法的注册过程分析:Dalvik虚拟机在调用一个成员函数的时候,如果发现该成员函数是一个JNI方法,那么就会直接跳到它的地址去执行。也就是说,JNI方法是直接在本地操作系统上执行的,而不是由Dalvik虚拟机解释器执行。由此也可看出,JNI方法是Android应用程序与本地操作系统直接进行通信的一个手段。 
  在Android系统中,JNI方法是以C/C++语言来实现的,然后编译在一个SO文件里面。这个JNI方法在能够被调用之前,首先要加载到当前应用程序进程的地址空间来。 
  Runtime类的成员函数nativeLoad是一个JNI方法。由于该JNI方法是属于Java核心类Runtime的,也就是说,它在Dalvik虚拟机启动的时候就已经在内部注册过了,因此,这时候我们可以直接调用它注册其它的JNI方法,也就是so文件filename里面所指定的JNI方法。 
  参数env所指向的一个JNIEnv结构体,通过调用这个JNIEnv结构体可以获得参数className所描述的一个类。这个类就是要注册JNI的类,而它所要注册的JNI就是由参数gMethods来描述的。 
  注册参数gMethods所描述的JNI方法是通过调用env所指向的一个JNIEnv结构体的成员函数RegisterNatives来实现的,因此,接下来我们就继续分析它的实现。 
  函数RegisterNatives首先是调用函数dvmDecodeIndirectRef来获得要注册JNI方法的类对象,接着再通过一个for循环来依次调用函数dvmRegisterJNIMethod注册参数methods描述所描述的每一个JNI方法。注意,每一个JNI方法都由名称、签名和地址来描述。 
  一个JNI方法是可以重复注册的,无论如何,函数dvmRegisterJNIMethod都是调用另外一个函数dvmUseJNIBridge来继续执行注册JNI的操作。 
  一个JNI方法并不是直接被调用的,而是通过由Dalvik虚拟机间接地调用,这个用来间接调用JNI方法的函数就称为一个Bridge。这些Bridage函数在真正调用JNI方法之前,会执行一些通用的初始化工作。例如,会将当前线程的状态设置为NATIVE,因为它即将要执行一个Native函数。又如,会为即将要被调用的JNI方法准备好前面两个参数,第一个参数是一个JNIEnv对象,用来描述当前线程的Java环境,通过它可以访问反过来访问Java代码和Java对象,第二个参数是一个jobject对象,用来描述当前正在执行JNI方法的Java对象。 
  这些Bridage函数实际上仍然不是直接调用地调用JNI方法的,这是因为Dalvik虚拟机是可以运行在各种不同的平台之上,而每一种平台可能都定义有自己的一套函数调用规范,也就是所谓的ABIApplication Binary Interface),这是一个APIApplication Programming Interface)不同的概念。ABI是在二进制级别上定义的一套函数调用规范,例如参数是通过寄存器来传递还是堆栈来传递,而API定义是一个应用程序编程接口规范。换句话说,API定义了源代码和库之间的接口,因此同样的代码可以在支持这个API的任何系统中编译 ,而ABI允许编译好的目标代码在使用兼容ABI的系统中无需改动就能运行。  
   Dalvik虚拟机提供的Bridge函数主要是分为两类。第一类Bridge函数在调用完成JNI方法之后,会检查该JNI方法的返回结果是否与声明的一致,这是因为一个声明返回StringJNI方法在执行时返回的可能会是一个Byte Array。如果不一致,取决于Dalvik虚拟机的启动选项,它可能会停机。第二类Bridge函数不对JNI方法的返回结果进行上述检查。选择哪一类Bridge函数可以通过-Xcheck:jni选项来决定。不过由于检查一个JNI方法的返回结果是否与声明的一致是很耗时的,因此,我们一般都不会使用第一类Bridge函数。 
    此外,每一类Bridge函数又分为四个子类:GenernalSyncVirtualNoRefStaticNoRef。 每一类Bridge函数之所以要划分为上述四个子类,是因为每一个子类的Bridge函数在调用真正的JNI方法之前,所要进行的准备工作是不一样的。例如,Genernal类型的Bridge函数需要为引用类型的参数增加一个本地引用,避免它在JNI方法执行的过程中被回收。又如,Sync类型的Bridge函数在调用JNI方法之前,需要执行同步原始,以避免多线程访问的竞争问题。 
  至此,我们就分析完成Dalvik虚拟机JNI方法的注册过程了。这样,我们就打通了Java代码和Native代码之间的道路。实际上,很多JavaAndroid核心类的功能都是通过本地操作系统提供的系统调用来完成的,例如,Zygote类的成员函数forkAndSpecialize最终是通过Linux系统调用fork来创建一个Android应用程序进程的,又如,Thread类的成员函数start最终是通过pthread线程库函数pthread_create来创建一个Android应用程序线程的。 
   
  Android应用程序资源的编译和打包过程分析: 
  在打包之前,大部分文本格式的XML资源文件还会被编译成二进制格式的XML资源文件。XML资源要二进制格式,是因为:二进制格式的XML文件占用空间更小。这是由于所有XML元素的标签、属性名称、属性值和内容所涉及到的字符串都会被统一收集到一个字符串资源池中去,并且会去重。有了这个字符串资源池,原来使用字符串的地方就会被替换成一个索引到字符串资源池的整数值,从而可以减少文件的大小。二进制格式的XML文件解析速度更快。这是由于二进制格式的XML元素里面不再包含有字符串值,因此就避免了进行字符串解析,从而提高速度。 
  为了使得一个应用程序能够在运行时同时支持不同的大小和密度的屏幕,以及支持国际化,即支持不同的国家地区和语言,Android应用程序资源的组织方式有18个维度,每一个维度都代表一个配置信息,从而可以使得应用程序能够根据设备的当前配置信息来找到最匹配的资源来展现在UI
  18个维度,两个额外操作: 赋予每一个非assets资源一个ID值,这些ID值以常量的形式定义在一个R.java文件中。生成一个resources.arsc文件,用来描述那些具有ID值的资源的配置信息,它的内容就相当于是一个资源索引表。 
 
  Android控件TextView的实现原理分析
  应用程序窗口,即Activity窗口,是由一个PhoneWindow对象,一个DecorView对象,以及一个ViewRoot对象来描述的。其中,PhoneWindow对象用来描述窗口对象,DecorView对象用来描述窗口的顶层视图,ViewRoot对象除了用来与WindowManagerService服务通信之外,还用来接收用户输入。窗口控件本身也是一个视图,即一个View对象,它们是以树形结构组织在一起形成整个窗口的UI的。 
  Activity窗口的UI绘制操作分为三步来走,分别是测量、布局和绘制。 
  Java层的Canvas实际上是封装了C++层的SkCanvasC++层的SkCanvas内部有一块图形缓冲区,这块图形绘冲区就是窗口的绘图表面(Surface)里面的那块图形缓冲区。 
  窗口的绘图表面里面的那块图形缓冲区实际上是一块匿名共享内存,它是SurfaceFlinger服务负责创建的。SurfaceFlinger服务创建完成这块匿名共享内存之后,就会将其返回给窗口所运行在的进程。窗口所运行在的进程获得了这块匿名共享内存之后,就会映射到自己的进程空间来,因此,窗口的控件就可以在本进程内访问这块匿名共享内存了,实际上就是往这块匿名共享内存填入UI数据。注意,这个过程执行完成之后,控件的UI还没有反映到屏幕上来,因为这时候将控件的UI数据填入到图形缓冲区而已。 
  窗口的UI的显示是WindowManagerService服务来控制的。因此,当窗口的所有控件都绘制完成自己的UI之后,窗口就会向WindowManagerService服务发送一个Binder进程间程通信请求。WindowManagerService服务接收到这个Binder进程间程通信请求之后,就会请求SurfaceFlinger服务刷新相应的窗口的UI。 
  一个窗口的所有控件的UI都是绘制在窗口的绘图表面上的,也就是说,一个窗口的所有控件的UI数据都是填写在同一块图形缓冲区中;一个窗口的所有控件的UI的绘制操作是在主线程中执行的。 
  为什么要规定所有与UI相关的操作都必须在主线程中执行呢?我们知道,这些与UI相关的操作都涉及到大量的控件内部状态以及需要访问窗口的绘图表面,也就是说,要大量地访问控件类的成员变量以及窗口绘图表面里面的图形缓冲区,因此,如果不将这些与UI相关的操作限定在同一个线程中执行的话,那么就会涉及到线程同步问题。线程同步的开销是很大的,因此,就要保证那些与UI相关的操作都在同一个线程中执行。这个负责执行UI相关操作的线程便是应用程序进程的主线程,因此我们也将应用程序进程的主线程称为UI线程。 
  应用程序进程的主线程除了负责执行与UI相关的操作之外,还负责响应用户的输入。 
  那么,有没有办法让某一个控件的UI享有独立的图形缓冲区呢?也就是这个控件不将自己的UI数据填入到它的宿主窗口的绘图表面的图形绘冲区里面去。如果可以的话,那么我们就可以在另外一个独立的线程中绘制该控件的UI。这样做的好处是显而易见,可以在这个独立的线程执行相对比较耗时的UI绘制操作而不会导致主线程无法及时响应用户输入。答案是肯定的,在接下来的一篇文章中,我们就分析一个可以具有独立图形缓冲区的控件---SurfaceView。 
  每一个窗口的创建的时候,都会与系统的输入管理器建立一个用户输入接收通道。输入管理器在启动两个线程,其中一个用来监控用户输入,即监控用户是否按下或者放开了键盘按键,或者是否触摸了屏幕,另外一个用来将监控到的用户输入事件分发给当前激活的窗口来处理,而这个分发过程就是通过前面建立的通道来进行的。 
  当前激活的窗口接收到输入管理器分发过来的用户输入事件之后,就会把该事件封装成一个消息发送到当前激活的窗口所运行在的应用程序进程的主线程的消息队列中去。等到这个消息被处理的时候,就会调用与当前激活的窗口所关联的一个ViewRoot对象的成员函数deliverKeyEvent或者deliverPointerEvent来将前面接收到的用户输入分发给合适的控件。其中,ViewRoot类的成员函数deliverKeyEvent负责分发键盘输入事件,而ViewRoot类的成员函数deliverPointerEvent负责分发触摸屏输入事件。 
 
  sleep()nanosleep()都是使进程睡眠一段时间后被唤醒,但是二者的实现完全不同。Linux中并没有提供系统调用sleep()sleep()是在库函数中实现的,它是通过调用alarm()来设定报警时间,调用sigsuspend()将进程挂起在信号SIGALARM上,sleep()只能精确到秒级上。 
  nanosleep()则是Linux中的系统调用,它是使用定时器来实现的,该调用使调用进程睡眠,并往定时器队列上加入一个timer_list型定时器,time_list结构里包括唤醒时间以及唤醒后执行的函数,通过nanosleep()加入的定时器的执行函数仅仅完成唤醒当前进程的功能。系统通过一定的机制定时检查这些队列(比如通过系统调用陷入核心后,从核心返回用户态前,要检查当前进程的时间片是否已经耗尽,如果是则调用schedule()函数重新调度,该函数中就会检查定时器队列,另外慢中断返回前也会做此检查),如果定时时间已超过,则执行定时器指定的函数唤醒调用进程。当然,由于系统时间片可能丢失,所以nanosleep()精度也不是很高。 
  alarm()也是通过定时器实现的,但是其精度只精确到秒级,另外,它设置的定时器执行函数是在指定时间向当前进程发送SIGALRM信号。 


0 0