strcmp(argv[i],"--start-system-server")==0 :
false;
setArgv0(argv0,"zygote");
set_process_name("zygote");
runtime.start("com.android.internal.os.ZygoteInit",
startSystemServer);
}
//...
}
这里我们发现,--zygote和--start-system-server和init.rc中zygote相关的参数吻合,而最关键的代码是runtime.start,其原型是AndroidRuntime:
:
start(xxx,xxx)(注意,是带参数的start版本,创建zygote服务),具体大家可以查看frameworks/base/core/jni/AndroidRuntime.cpp这个文件。
我们来简单整理一下调用过程,runtime.start->startVm->JNI_CreateJavaVM。
在start(xxx,xxx)函数中,做了以下三件事情,创建Dalvik虚拟机、注册native函数和调用ZygoteInit.java中的main()函数。
创建Dalvik虚拟机
/*
*InitializetheVM.
*
*TheJavaVM*isessentiallyper-process,andtheJNIEnv*isper-thread.
*Ifthiscallsucceeds,theVMisready,andwecanstartissuing
*JNIcalls.
*/
if(JNI_CreateJavaVM(&mJavaVM,&env,&initArgs)<0){
LOGE("JNI_CreateJavaVMfailed\n");
gotobail;
}
严格来讲,Dalvik虚拟机并不是Java虚拟机,一个虚拟机只有通过JCK(JavaComplianceKit)的测试并获得授权后才能称为JVM。
DalvikVM非但无法直接运行.class文件(只能执行dx工具从.class文件转换过的.dex文件),更没有获得授权。
不过话说回来,Android之所有开发Dalvik,除了性能上的考虑外,很大一部分原因就是要避开Sun的授权。
这里我们只需要了解,JNI_CreateJavaVM()创建了Dalvik虚拟机的一个实例。
虚拟机就像薄薄的一层纸,粘附在linux进程上,任何Java代码要想跑起来,都必须经过这层纸。
注册native函数
/*
*Registerandroidfunctions.
*/
if(startReg(env)<0){
LOGE("Unabletoregisterallandroidnatives\n");
gotobail;
}
所有的native函数在这里注册。
在AndroidRuntime.cpp中有一个函数指针表gRegJNI[],里面存放了各个模块的nativefunctions注册函数,每个注册函数会在startReg()中被逐一运行,因而每个模块的nativefunctions也被注册到虚拟机中。
当这一步执行完后,也就意味着,系统已经准备好执行Java代码了。
这正是接下来我们要讲述的。
调用ZygoteInit.java中的main()函数
//slashClassName对应ZygoteInit类
startClass=env->FindClass(slashClassName);
//找到ZygoteInit类的main()函数
startMeth=env->GetStaticMethodID(startClass,"main","([Ljava/lang/String;)V");
//开始执行Java代码
env->CallStaticVoidMethod(startClass,startMeth,strArray);
从CallStaticVoidMethod()开始,Java代码就开始在Dalvik虚拟机的Java解释器(Interpreter)上运行起来了。
事实上,Dalvik虚拟机用C语言构建,就好比linux上的一个应用程序,它的Java解释器(也是一些C代码)维持一个while循环,不停的执行Java代码中的每一条bytecode指令。
执行效率直接依赖于Interpreter的效率,所以,在dalvik的实现中,有两套版本,一套是C版本,另一套是ASM版本,其中ASM版本又对应着不同的平台,如ARM/MIPS/X86。
MIPS的版本应该是MIPS公司在Google发布Android版本后添加进去的,因为从Android官方网站上取下的代码中并不包含MIPS的ASM版本,可见Android对MIPS的支持远远不如ARM。
main()函数都干了些什么
前面解释到ZygoteInit类的main()函数开始被执行了,下面我们看看main()都做了哪些事情。
registerZygoteSocket();
if(argv[1].equals("true")){
startSystemServer();
}
runSelectLoopMode();
registerZygoteSocket()为zygote进程注册socket机制,我们知道,zygote进程都是通过socket机制接受子进程创建的请求的。
startSystemServer()是一个相当重要的步骤,zygote进程生下了它的第一个蛋,systemserver。
这一子进程为android系统创建了大部分的系统服务,包括在native服务和Android服务。
Native服务包括:
SurfaceFlinger/AudioFlinge/mediaplaybackservice/cameraservice
Android服务就更多了,
EntropyService/PowerManager/ActivityManager/TelephonyRegistry/PackageManager/AccountManagerContentManager/SystemContentProviders/BatteryService/HardwareService/AlarmManager/InitWatchdogSensorService/WindowManager/BluetoothService/Statusbar/ClipboardService/InputMethodServiceNetStatService/ConnectivityService/AccessibilityManager/NotificationManager/MountService/DeviceStorageMonitorLocationManager/SearchService/CheckinService/WallpaperService/AudioService/HeadsetObserverBackupService/AppWidgetService
现在来看Android的启动序列图,相信你会有更感性和深刻的认识,
接下来执行的runSelectLoopMode()就是一只真正的老母鸡了,正是在这里,一个又一个子进程被复制出来。
在这个函数里面,有一个while(true)循环,zygote会一直等待请求,一旦发现socket发出请求,zygote马上执行ZygoteConnnection.java类的runOnce()函数,首先创建出子进程,然后是通过pid的值来区分对待,于是父子在这里开始分道扬镳,zygote仍旧尽职的等待下一个请求的链接,子进程则做它自己事情去了,这也是子进程被创建出来的意义所在。
while(true)
{
//等待socket请求
acceptCommandPeer();
ZygoteConnection:
:
runOnce();
}
|
|
|
|
________________________|
|
|
|
\/
pid=Zygote.forkAndSpecialize();//通过jni呼叫linux的系统调用fork()
if(pid==0){
handleChildProc();
returntrue;
}else{
returnhandleParentProc();
}
zygote本身是一个虚拟机进程,同时也是一个应用进程的孵化器,每当系统要求执行一个Android应用程序时(当需要运行manifest文件中的,,和中的类时,就会通过socket向zygote发送启动命令),zygote就会fork出一个子进程。
子进程同样是一个虚拟机进程,与zygote父进程共享代码空间,但是数据空间是互相独立的,子进程数据空间中的内容是父进程的完整拷贝(包括和zygote一样的Dalvik虚拟机运行环境,程序计数器-pc也相同)。
只有一点不同,如果fork成功,子进程中fork的返回值是0,父进程中fork的返回值是子进程的进程号,因为也就有了上述代码的分叉执行。
子进程继续加载java程序的类和方法,并通过JavaInterpreter最终来解释并运行java程序。
这样做的好处显而易见:
zygote进程是在系统启动时产生的,它会完成虚拟机的初始化,库的加载,预置类库的加载和初始化等等操作,而在系统需要一个新的虚拟机实例时,Zygote通过复制自身,最快速的提供给系统。
另外,对于一些只读的系统库,所有虚拟机实例都和Zygote共享一块内存区域,大大节省了内存开销。
小结:
本节用一定的篇幅介绍了Android系统启动时Dalvik初始化的时机,以及zygote和虚拟机之间的那些事儿。
当提到Dalvik虚拟机时,我们不再感到茫然陌生,因为我们的脑海中已经有了一个虚拟机工作的模型,尽管模糊,却可触摸。
接下来我们将用一系列的文章深入Dalvik虚拟机,钻研其原理,关注其实现,展望其未来,敬请期待。
如无特别提及,文中所指均为Android2.2(froyo)的Dalvik实现。
[edit]源代码树
dalvik/
|
|----------dalvikvm/--Dalvikvm工具的源代码,针对某个类单独运行虚拟机
|
|----------dexdump/--dexdump工具的源代码,类似于objdump
|
|----------dexlist/--dexlist工具的源代码,可列出dex文件中所有类的method
|
|----------dexopt/--Dalvik优化工具,可以对加载的dex文件进行验证与优化,生成一个classes.dex文件,
|如:
/data/dalvik-cache/system@framework@ext.jar@classes.dex
|
|----------docs/--dalvik的一些设计文档
|
|----------dvz/
|
|----------dx/--dx负责将Java的.class文件转换成Dalvik可执行的.dex文件,这里是源代码
|
|----------hit/
|
|----------libcore/--核心库的源代码主要本目录和dalvik/vm/native中,很多都是来自ApacheHarmony项目,也有一些是来自OpenSSL、zlib和ICU等项目
|
|----------libdex/--.dexfile的加载和解析,值得一提的是,InstrUtils.c/h提供了对Dalvik虚拟机指令的解析,比如解析操作码和操作数
|
|----------libnativehelper/
|
|----------tests/
|
|----------tools/
|
|----------vm/--Dalvik的源代码,研究Dalvik主要关注这个目录
||
||----------analysis/--class/method的分析/验证/优化
||
||----------arch/--JNIcallbridge,从Java端调用native函数时,需要根据不同平台对C函数的参数顺序和返回结果进行调整,比如,
||x86的参数是从右往左推入栈中,结果通过eax(必要的话,加上edx)返回,
||arm的参数从左到右,依次放入r0,r1,r2,r3寄存器,大于4个的参数从右往左压入栈中,结果通过r0(必要的话,加上r1)。
||若使用的平台不在JNIcallBrige支持范围时,系统会使用libffi库,该库性能稍差。
||
||----------hprof/--heapprofile调试工具的相关实现
||
||----------interp/--Interpreter的主架构实现,调用mterp目录中的bytecode,完成虚拟机指令的执行
||Stack.c定义了Dalvik栈的初始化,帧入栈,帧出栈和栈的dump等操作
||Jit.c/h中定义了供外界调用的接口
||
||----------jdwp/--JavaDebugWireProtocol的相关实现
||
||----------mterp/--Interpreter的bytecode功能实现,包括C和ASM版本
||
||----------native/--核心库的源代码主要本目录和dalvik/libcore中,很多都是来自ApacheHarmony项目,也有一些是来自OpenSSL、zlib和ICU等项目
||
||----------oo/--处理一些面向对象的特性,如解析加载Java类,创建虚拟函数表vtable,接口表iftable,
||解析加载directMethods,virtualMethods,sfields和ifields,
||类hash表gDvm.loadedClasses的创建/查找/销毁/扫描(为GC回收做标记而扫描),ClassObject的dump等
||
||----------reflect/--实现Java的反射机制,该特性允许运行中的Java程序对自身检查,并直接操作程序的内部属性
||
||----------test/
||
||----------CheckJni.c--对Object/Class/String有效性的验证,更重要的是,提供了gCheckNativeInterface[]和gCheckInvokeInterface[]
||这两个表被挂载到JNIEnvExt和JavaVMExt数据结构中的funcTable指针
||
||----------Hash.c--对hash表的操作,包括:
创建/清除/释放/扩展/移除/查询/遍历等
||
||----------Init.c/Jni.c--Dalvik的初始化代码皆在此,包括JNI_CreateJavaVM
||
||----------LinearAlloc.c--从ashmem划出一块“dalvik-LinearAlloc”内存区域,大小为4M,专门用于存放那些只写一次的数据,
||比如class的vtables,fields,methods,interfaces等
||
||----------Native.c--在解释器执行Java代码时,如碰到nativemethod,需怎样定位和执行,本文件给出了答案
||
||----------ReferenceTable.c--引用表的初始化/销毁/dump,对象引用的入表/移除,查询和对比
||
||----------Thread.c--线程的操作,包括:
线程的创建/销毁,挂起/继续,dump/scan(为GC标记而进行的扫描)等
||
||
||----------alloc/--Dalvik内存分配和垃圾回收的实现源代码
|||
|||----------Heap.c--整个GcHeap的初始化/销毁,内存的分配,GC的触发
|||
|||----------HeapSource.c--一个HeapSource最多可拥有3个heap,所有的对象都是从这几个heap中分配,调用dlmalloc模块
|||
|||----------HeapWorker.c--HeapWorker线程执行某些对象的finalize(),在GC回收时被唤醒
|||
|||----------HeapTable.c--对引用表的操作实现,比如初始化/释放,加入/移除对象和对表中对象的标记
|||
|||----------HeapBitmap.c/h--堆位图(HeapBitmap)的分配/释放/遍历/查找/置位/清位,堆位图空间中的一个bit对应堆上的8个字节
|||堆位图空间在androidsharedmemory中分配,通过mmap()映射到本进程
|||
|||----------MarkSweep.c--对象的标记-清除算法实现
||
||
||----------compiler/--JIT的相关实现
|||
|||----------Compiler.c--compiler线程的代码实现,以及编译任务队列的管理
|||
|||----------Frontend.c--method/trace单元的编译函数实现
|||
|||----------Dataflow.c--编译框架的实现
|||IntermediateRep.c
|||Loop.c
|||Utility.c
|||
|||----------codegen/--不同架构的编译实现,如arm,x86,mips
|||
|||----------template/--
|||
[edit]基于栈还是基于寄存器
首先一点,指令由操作码(opcode)和操作数(operand)组成。
当我们谈及虚拟机是栈架构还是寄存器架构时,是针对虚拟机指令中操作数的访问方式而言的。
我们看一行简单的代码,
a=b+c
在基于寄存器的指令集架构(DalvikVM)中,该行代码将被翻译成下面的指令,大家看到之后是否感觉很熟悉?
因为平常所见的处理器如ARM/MIPS/X86等的指令集都基于寄存器(当然,虚拟机指令中的虚拟寄存器和处理器中的物理寄存器不同,虚拟寄存器可能被存放在内存中,也有可能被映射成处理器的物理寄存器,视实现而定)。
在这类指令集中,源操作数和目的操作数需要在指令码中显式指定,由于操作数个数不定,导致指令码的长度长短不同,因而在执行时需要更多的内存空间。
IADDa,b,c
在基于栈的指令集架构(JVM)中,操作数从Java虚拟栈中的操作数栈(operandstack)取得,伴随着指令的执行,不断的有操作数出入栈。
在下面的Java字节码中,前两条字节码指令从Java虚拟栈的局部变量区读取b和c的值,并推入操作数栈,IADD从操作数栈弹出2个值,相加,然后把结果推入栈顶,ISTORE从操作数栈弹出一个值,保存到局部变量区。
由于字节码无需在指令中指定操作数,因而字节码的密度可以非常高,但是,完成同样的工作,基于栈架构的系统比基于寄存器架构的系统需要更多条的指令。
ILOADc
ILOADb
IADD
ISTOREa
网上一些文章在讨论Dalvik时,大都简单提及Dalvik执行速度比JVM快,但移植性