Dalvik之非常简介
最近对应用层有些懈怠,决定深入一点从虚拟机研究入手,增加一些难度hhhh
分析老罗的代码为主,建议大家都仔细学习一下,以学习记录思想。
一张图可以说明大概了。
image.png启动过程
在Android系统中,应用程序进程都是由Zygote进程孵化出来的。Zygote进程在启动时会创建一个Dalvik虚拟机实例,每当它孵化一个新的应用程序进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面去,从而使得每一个应用程序进程都有一个独立的Dalvik虚拟机实例。
JavaVMExt
虚拟机实例。拥有一个函数表FuncTable
--gInvokeInterface
,它是线程与虚拟机交互的接口。更具体的说,应该是整个进程与虚拟机交互的接口。例如:
- AttachCurrentThread
- DestroyJavaVM
当前线程可以通过这个接口挂载到虚拟机中。
JNIEnvExt
JNIEnvExt
通过调用函数dvmCreateJNIEnv
来完成的。每一个Dalvik
虚拟机实例会有一个JNI
环境列表,用envList
表示。首先说Zygote
中的主线程,在创建虚拟机时,就会在虚拟机中创造一个JNI环境
。每一个Attach
到Dalvik
虚拟机中去的线程都有一个对应的JNIEnvExt
。JNIEnvExt
也有自己的函数表gNativeInterface
,这是C++
调用Java
的本地接口表。例如FindClass
,就是C++
希望得到Java
中的某个类。
其实怎么说,我们首先要明确一个事实,Dalvik
虚拟机实例是用来执行Java
代码的,而JNI
代码其实是由本地操作系统环境来执行的。所以当本地Native
代码希望获取Java
中的某些信息时,就需要先获取当前线程的JNI
环境(JNIEnv
),然后调用它的函数表接口来获取。
注册核心方法
启动时还需要预先注册一些Android核心类JNI方法
。JNI
方法一般都定义在so
中,所以系统会先加载so
,并注册JNI
方法。注册JNI
方法是什么意思呢,可以先大致说一下整个过程,跟linux
加载so
的过程类似,首先dlopen
加载so
获取句柄,然后dlsym
找到定义的JNI_Onload
方法,最后通过JNIEnv
进入dalvik
虚拟机内部进行registerNativeMethod
注册JNI
方法。
每一个用来实现JNI方法的so文件都应该定义有一个名称为JNI_OnLoad
的函数:
jint JNI_OnLoad(JavaVM* vm, void* reserved);
注册方法时,通过
(*env)->RegisterNatives
进入虚拟机内部进行注册。我们首先会获得一个Method
对象method
,用来描述要注册的JNI
方法所对应的Java
类成员函数,当JNI
方法未被注册时,method->nativeFunc
指向dvmResolveNativeMethod
。当真正注册时,会为方法选择一个合适的bridge
函数。即设置method->nativeFunc = xxxbridge
调用JNI方法
当未来在Java
层调用JNI
方法,虚拟机会通过Bridge
进行调用
这些Bridge函数实际上仍然不是直接调用地调用JNI方法的,这是因为Dalvik虚拟机是可以运行在各种不同的平台之上,而每一种平台可能都定义有自己的一套函数调用规范,也就是所谓的ABI(Application Binary Interface),这是一个API(Application Programming Interface)不同的概念。ABI是在二进制级别上定义的一套函数调用规范,例如参数是通过寄存器来传递还是堆栈来传递,而API定义是一个应用程序编程接口规范。换句话说,API定义了源代码和库之间的接口,因此同样的代码可以在支持这个API的任何系统中编译 ,而ABI允许编译好的目标代码在使用兼容ABI的系统中无需改动就能运行。
为了使得运行在不同平台上的Dalvik虚拟机能够以统一的方法来调用JNI方法,这些Bridge函数使用了一个libffi库,它的源代码位于external/libffi目录中。Libffi是一个开源项目,用于高级语言之间的相互调用的处理,它的实现机制可以进一步参考http://www.sourceware.org/libffi/。
总而言之,Bridge
会使用函数dvmPlatformInvoke
通过libffi
库来调用对应的JNI方法,以屏蔽Dalvik
虚拟机运行在不同目标平台的细节。
执行过程
虚拟机实例启动完毕后,肯定会有一个main
函数来执行。因此zygote
会通过JNIEnv.CallStaticVoidMethod
来执行main
函数。这里是不是跟我们前面说的相呼应
以当本地native代码希望获取java中的某些信息时,就需要先获取当前线程的JNI环境(JNIEnv),然后调用它的函数表接口来获取。
native方法
如果方法是native
方法,那么Method->nativeFunc
就是方法的地址,直接执行就ok。走前面讲的JNI
方法的Bridge
执行过程。
Java方法
如果方法是Java
方法,就通过dvmInterpret
执行。真正发挥虚拟机的解析作用,首先,虚拟机实例有三种执行Java
方法的模式:
- Portable 可移植模式
- Fast 快速模式
- JIT JIT模式
执行过程看起来挺简单,
pc程序计数器指向的就是当前要执行的Java函数的方法区,在一个无限while循环中,通过FETCH宏依次获得当前程序计数器(pc)中的指令inst,并且通过宏INST_INST获得指令inst的类型,最后就switch到对应的分支去解释指令inst。
以Zygote进程为例,Dalvik虚拟机解释器就是以com.android.internal.os.ZygoteInit类的静态成员函数main为入口点执行,然后在一个Socket上进行循环,用来等待和处理ActivityManagerService服务向它发送创建新应用程序进程的请求,直至系统退出为止。又以Android应用程序进程为例,Dalvik虚拟机解释器就是以android.app.ActivityThread类的静态成员函数main为入口点执行,然后在一消息队列上进行循环,用来等待和处理主线程的消息,直到应用程序退出为止。
进程与线程
最后说一下虚拟机中的进程与线程。Dalvik
虚拟机进程实际上就是通常我们所说的Android
应用程序进程。通过Zygote
的fork
完成。一个Dalvik
虚拟机进程实际上就是一个Linux
进程.在Java
代码中,我们可以通过java.lang.Thread
类的成员函数start
来创建一个Dalvik
虚拟机线程,在Dalvik
虚拟机中对应有一个Native
层的Thread
对象。它本质通过本地OS的函数pthread_create
来创建一个线程,Dalvik
虚拟机线程实际上就是本地操作系统线程。
同时如果该线程是在Java
层创建,或者在Native
层创建但需要与Java
层交互,就会与前面的主线程类似,通过dvmCreateJNIEnv
来为新创建的Dalvik
虚拟机线程创建一个JNI
环境。同时,线程需要执行时,就会通过JNIEnv
中的接口dvmCallMethod
让虚拟机执行Java
层中的Thread.run
虚拟机线程会有三个重要的引用表:
- JNI本地引用表。
JNI
方法引用Java
对象时,会往当前Dalvik虚拟机线程的JNI方法本地引用表添加一个引用,以便它们不会被GC回收。 - Dalvik虚拟机内部引用表。在Dalvik虚拟机内部为线程创建一些对象,这些对象需要添加到一个Dalvik虚拟机内部引用表中去,以便在线程退出时,可以对它们进行清理。
- Monitor引用表。同步的一些东西