深入理解Android之Java虚拟机Dalvik

Android开发系列 专栏收录该内容
115 篇文章 59 订阅

一、背景

这个选题很大,但并不是一开始就有这么高大上的追求。最初之时,只是源于对Xposed的好奇。Xposed几乎是定制ROM的神器软件技术架构或者说方法了。它到底是怎么实现呢?我本意就是想搞明白Xposed的实现原理,但随着代码研究的深入,我发现如果不了解虚拟机的实现,而仅简单停留在Xposed的调用流程之上,那真是对Xposed最大的不敬了。另外,歪果仁为什么能写出Xposed?Android上的Java虚拟机对他们来说应该也是相对陌生的,何以他们能做而我们没有人搞出这样的东西?

所以,在研究Xposed之后,我决定把虚拟机方面的东西也来研究一番。诚如我在很多场合中提到的关于Android学习的三个终极问题(其实对其他各科学习也适用):学什么?怎么学?学到什么程度为止?关于这三个问题,以本次研究的情况来看,回答如下:

  • 学习目标是:按顺序是dalvik虚拟机,然后是Xposed针对dalvik的实现,然后是art虚拟机。
  • 学习方法:VM原理配合具体实现,以代码为主。Java VM有一套规范,各公司具体的VM实现必须遵守此规范。所以对VM学习而言,规范很重要,它是不变的,而代码实现只不过是该规范的一种实现罢了。这里也直接体现了我提出的关于专业知识学习的一句警语“基于Android,高于Android”。对VM而言,先掌握规范才是最最重要和核心的事情。
  • 学到什么程度为止:对于dalvik虚拟机,我们以学会一段Java程序从代码,到字节码,最后到如何被VM加载并运行它为止。关于dalvik的内存管理我们不会介绍。对于XPosed,基于dalvik+selinux环境的代码我们会全部分析。对于ART,由于它是Google未来较长一段时期的重点,所以我们也会围绕它做更多的分析,诸如内存管理怕是肯定会加上的。

除了这三个问题,其实还有一个隐含的疑问,学完之后有什么用呢?

  • 这个问题的答案要看各位的需求了。从本人角度来看,我就是想知道Xposed是怎么实现的。另外,如果了解虚拟机实现的话,我还想定制它,使得它在智能POS领域变得更安全一点。
  • 当然,我自己有一个比较高大上的梦想,就是我一直想写Linux Kernel方面的书,而且我自认为已经找到了一个绝妙的学习它的入手点(我在魅族做分享的时候介绍过。到今天为止一年多过去了,不知道当初的有心人是否借此脱引而出,如果有的话也请和大家分享下你的学习经历)。Anyway,从目前的工作环境和需求来看,VM是当前更好的学习目标。

言归正传,现在开始正式介绍dalvik,请牢记关于它的学习目标和学习程度。

你也可以下载本专题对应的demo代码用于学习。

二、Class、dex、odex文件结构

2.1  Class文件结构总览

Class文件是理解Vm实现的关键。关于Class文件的结构,这里介绍的内容直接参考JVM规范,因为它是最权威的资料。

Oracle的JVM SE7官方规范:https://docs.oracle.com/javase/specs/jvms/se7/html/

还算很有良心,纯网页版的,也可以下载PDF版。另外,周志明老师曾经翻译过中文版的JVM规范,网上可搜索到。

作为分析Class文件的入口,我在Demo示例中提供了一个特别简单的例子,代码如图1所示:

TestMain类的代码简单到不行,此处也不拟多说,因为没有特殊之处。

当我们用eclipse编译这个类后,将得到bin/com/test/TestMain.class。这个TestMain.class就是我们要分析的Class文件了。

Class文件到底是什么东西?我觉得一种通俗易懂的解释就是:

  1. *.java文件是人编写的,给人看的。
  2.  *.class是通过工具处理*.java文件后的产物,它是给VM看的,给VM操作的

在某种哲学意义上看,java源文件和处理得到的class文件是同一种东西......

那么,这个给VM使用的class文件,其内部结构是怎样的呢?Jvm规范很聪明,它通过一个C的数据结构表达了class文件结构。这个数据结构如图2所示:

请大家务必驻足停留片刻,因为搞清楚图2的内容对后续的学习非常关键。图2的ClassFile这个数据结构真得是太容易理解了。相比那些native的二进制程序而言,ClassFile的组织结构和Java源码的组织结构匹配度非常高,以致于我第一眼看到这个结构体时,我觉得自己差不多就理解了它:

  • 比如,类的是public的还是final的,还是interface,就由access_flags来表示。其具体取值我觉得都不用管,代码中用得是名字诸如ACC_XXX这样得的标志位来表示,一看知道是啥玩意儿。
  • Java类中定义的域(成员变量),方法等都有对应的数据结构来表达,而且还是个数组。
  • 唯一有点特别之处的是常量池。什么东西会放在常量池呢?最容易想到的就是字符串了。对头,这个Java源码中的类名,方法名,变量名,居然都是以字符串形式存储在常量池中。所以,图2中的this_class和super_class分别指向两个字符串,代表本类的名字和基类的名字。这两个字符串存储在常量池中,所以this_class和super_class的类型都是u2(索引,代表长度为2个字节)。

Class文件用javap工具可以很好得解析成图2那样的格式,我这里替大家解析了一把,结果如图3所示(先显示部分内容):

注意,解析方法为:javap -verbose xxxx.class

先来看看常量池。

2.1.1  常量池介绍

常量池看起来陌生,其实简单得要死。注意,count_pool_count是常量池数组长度+1。比如,假设某个Class文件常量池只有4个元素,那么count_pool_count=5)。

javap解析class文件的时候,常量池的索引从1算起,0默认是给VM自己用得,一般不显示0这一项。这也是为什么图3中常量池第一个元素以#1开头。所以,如果count_pool_count=5的话,真正有用的元素是从count_pool[1]到count_pool[4]。

常量池数组的元素类型由下面的代码表示:

cp_info { //特别注意,这是介绍的cp_info是相关元素类型的通用表达。
    u1 tag;   //tag为1个字节长。不论cp_info具体是哪种,第一个字节一定代表tag
    u1 info[]; //其他信息,长度随tag不同而不同
}

//tag取值,先列几个简单的:
tag=7 <==info代表这个cp_info是CONSTANT_Class_info结构体
tag=9<==info代表CONSTANT_Fieldrefs_info结构体
tag=10<==info代表CONSTANT_Methodrefs_info结构体
tag=8<==info代表CONSTANT_String_info结构体
tag=1<==info代表CONSTANT_Utf8_info结构体

在JVM规范中,真正代表字符串的数据结构是CONSTANT_Utf8_info结构体,它的结构如下代码所示:

CONSTANT_Utf8_info {
    u1 tag;
    u2 length;  //下面就是存储UTF8字符串的地方了
    u1 bytes[length];
}

大家看图3中常量池的内容,比如#2=Utf8 com/test/TestMain  这行表示:

数组第二个元素的类型是CONSTANT_Utf8_info,字符串为“com/test/TestMain”

下面我们看几个常用的常量池元素类型

(1)  CONSTANT_Class_info

这个类型是用于描述类信息的,此处的类信息很简单,就是类名(也就是代表类名的字符串)

CONSTANT_Class_info {
    u1 tag;   //tag取值为7,代表CONSTANT_Class_info
    u2 name_index;  //name_index表示代表自己类名的字符串信息位于于常量池数组中哪一个,也就是索引
}

唉,够懒的,name_index对应的那个常量池元素必须是CONSTANT_Utf8_info,也就是字符串。图3中的例子,咱们再看看:

#1 = Class  #2  //com/test/TestMain

#2 = Utf8  com/test/TestMain

这说明:

  1. 常量池第一个元素类型为Class_info,它对应的name_index取值为2,表示使用第2个元素
  2. 常量池第二个元素类型为Utf8  内容为“com/test/TestMain”
  3. #1最后的//表示注释,它把第二行的字符串内容直接搬过来,方便我们查看
(2)  CONSTANT_NameAndType_Info

这个结构也是常量池数据结构中中比较重要的一个,干什么用得呢?恩,它用来描述方法/成员名以及类型信息的。有点JNI基础的童鞋相信不难明白,在JNI中,一个类的成员函数或成员变量都可以由这个类名字符串+函数名字符串+参数类型字符串+返回值类型来确定(如果是成员变量,就是类名字符串+变量名字符串+类型字符串)来表达。既然是字符串,那么NameAndType_Info也就是存储了对应字符串在常量池数组中的索引:

CONSTANT_NameAndType_info {
   u1 tag;
   u2 name_index;  //方法名或域名对应的字符串索引
   u2 descriptor_index; //方法信息(参数+返回值),或者成员变量的信息(类型)对应的字符串索引
}
//还是来看图3中的例子吧
#13 = Utf8  ()V
#15 = NameAnType  #16.#13  //合起来就是test.()V 函数名是test,参数和返回值是()V
#16=Utf8 test

太简单了,都不惜得说...,请大家自行解析#25这个常量池元素的内容,一定要做喔!

注意,对于构造函数和类初始化函数来说,JVM要求函数名必须是<init>和<cinit>。当然,这两个函数是编译器生成的。

(3)  CONSTANT_MethodrefInfo三兄弟

Methodref_Info还有两个兄弟,分别是Fieldref_Info,InterfaceMethodref_Info,他们三用于描述方法、成员变量和接口信息。刚才的NameAndType_Info其实已经描述了方法和成员变量信息的一部分,唯一还缺的就是没有地方描述它们属于哪个类。而咱这三兄弟就补全了这些信息。他们三的数据结构如图4所示:

如此直白简单,不解释了。不放心的童鞋们请对照图3的例子自行玩耍!

常量池先介绍到这,它还有一些有用的信息,不过要等到后面我们碰到具体问题时再分析

2.1.2  Field和Method描述

刚才在常量池介绍中有提到Methodref_Info和Fieldref_Info,不过这两个Info无非是描述了函数或成员变量的名字,参数,类型等信息。但是真正的方法、成员变量信息还包括比如访问权限,注解,源代码位置等。对于方法来说,更重要的还包括其函数功能(即这个函数对应的字节码)。

在Java VM中,方法和成员变量的完整描述由如图5所示的数据结构来表达的:

  • access_flags:描述诸如final,static,public这样的访问标志
  • name_index:方法或成员变量名在常量池中对应的索引,类型是Utf8_Info
  • attribute_info:是域或方法中很重要的信息。我们单独用一节来介绍它。

2.1.3  attribute_info介绍

attribute_info结构体很简单,如下代码所示:

attribute_info {//特别注意,这里描述的attribute_info结构体也是具体属性数据结构的通用表达
    u2 attribute_name_index;  //attribute_info的描述,指向常量池的字符串
    u4 attribute_length;  //具体的内容由info数组描述
    u1 info[attribute_length];
}

Java VM规范中,attribute类型比较多,我们重点介绍几个,先来看代表一个函数实际内容的Code属性。

(1)  Code属性

代表Code属性的数据结构如图6所示:

  • 前2个成员变量就不多说了。属于attribute的头6个字节,分别指向代表属性名字符串的常量池元素以及后续属性数据的长度。注意,Code属性的attribute_name_index所指向的那个Utf8常量池元素对应的字符串内容就是“Code”,大家可参考图3的#9。
  • max_stack和max_locals:虚拟机在执行一个函数的时候,会为它建立一个操作数栈。执行过程中的参数啊,一些计算值啊等都会压入栈中。max_stack就表示该函数执行时,这个栈的最大深度。这是编译时就能确定的。max_locals用于描述这个方法最大的栈数和最大的本地变量个数。本地变量个数包括传入的参数。
  • code_length和code:这个函数编译成Java字节码后对应的字节码长度和内容。
  • exception_table_length:用来描述该方法对应异常处理的信息。这块我不打算讲了,其实也蛮简单,就是用start_pc表示异常处理时候从此方法对应字节码(由code[]数组表示)哪个地方开始执行。
  • Code属性本身还能包含一些属性,这是由attributes_count和attributes数组决定的。

来看个实际例子吧,如图7所示(接着图3的例子):

图7中:

  • stack=2,locals=2,args_size=1。结合代码,main函数确实有一个参数,而且还有一个本地变量。注意,main函数是static的。如果对于类的非static函数,那么locals的第0个元素代表this。
  • stack后面接下来的就是code数组,也就是这个函数对应的执行代码。0表示code[]的索引位置。0:new:代表这个操作是new操作,此操作对应的字节码长度为3,所以下一个操作对应的字节码从索引3开始。
  • LineNumberTable也是属性的一种,用于调试,它将源码和字节码匹配了起来。比如line 7: 0这句话代表该函数字节码0那一个操作对应代码的第7行。
  • LocalVariableTable:它也是属性一种,用于调试,它用于描述函数执行时的变量信息。比如图7中的Start = 0:表示从code[]第0个字节开始,Length = 13表示到从start=0到start+13个字节(不包含第13个字节,因为code数组一共就12个字节)这段范围内,这个变量都有效(也就是这个变量的作用域),Slot=0表示这个变量在本地变量表中第一个元素,还记得前面提到的locals吗?,name为“args”,表示这个参数的名字叫args,类型(由Signature表示)就是String数组了。

请大家自行解析图7中最后一行,看看能搞明白LocalVariableTable的含义不...

另外,Android SDK build Tools中的dx工具dump class文件得到的信息更全,大家可以试试。

使用方法是:dx --dump --debug xxx.class。

Class文件先介绍到这,下面我们来看看Android平台上的dex文件。

2.2  Dex文件结构和Odex

2.2.1  dex文件结构简介

Android平台中没有直接使用Class文件格式,因为早期的Anrdroid手机内存,存储都比较小,而Class文件显然有很多可以优化的地方,比如每个Class文件都有一个常量池,里边存储了一些字符串。一串内容完全相同的字符串很有可能在不同的Class文件的常量池中存在,这就是一个可以优化的地方。当然,Dex文件结构和Class文件结构差异的地方还很多,但是从携带的信息上来看,Dex和Class文件是一致的。所以,你了解了Class文件(作为Java VM官方Spec的标准),Dex文件结构只不过是一个变种罢了(从学习到什么程度为止的问题来看,如果不是要自己来解析Dex文件,或者反编译/修改dex文件,我觉得大致了解下Dex文件结构的情况就可以了)。图8所示为Dex文件结构的概貌:


有一点需要说明:传统Class文件是一个Java源码文件会生成一个.Class文件,而Android是把所有Class文件进行合并,优化,然后生成一个最终的class.dex,如此,多个Class文件里如果有重复的字符串,当把它们都放到一个dex文件的时候,只要一份就可以了嘛。

dex头部信息中的magic取值为“dex\n035\0”

proto_ids:描述函数原型信息,包括返回值,参数信息。比如“test:()V”

methods_ids:函数信息,包括所属类及对应的proto信息。比如

"Lcom.test.TestMain. test:()V",.前面是类信息,后面属于proto信息

下面我们将示例TestMain.class转换成dex文件,然后再用dexdump工具看看它的结果,如图9所示:

具体方法:

  • 先将.class文件转换成dex文件,工具是sdk build-tools下的dx命令。dx --dex --debug --verbose-dump --output=test.dex com/test/TestMain.class,生成test.dex文件。
  • 同样,利用build-tools下的dexdump命令查看,dexdump -d -l plain test.dex,得到图9的结果

图9中的dexdump结果其实比图3还要清晰易懂。我们重点关注code段的内容(图中红框的部分):

  • registers:Dalvik最初目标是运行在以ARM做CPU的机器上的,ARM芯片的一个主要特点是寄存器多。寄存器多的话有好处,就是可以把操作数放在寄存器里,而不是像传统VM一样放在栈中。自然,操作寄存器是比操作内存(栈嘛,其实就是一块内存区域)快。registers变量表示该方法运行过程中会使用多少个寄存器。
  • ins:输入参数对应的个数,outs:此函数内部调用其他函数,需要的参数个数。
  • insns:size:以4字节为单位,代表该函数字节码的长度(类似Class文件的code[]数组)

Android官方文档:https://source.android.com/devices/tech/dalvik/dex-format.html

说实话,写完这一小节的时候,我又反复看了官方文档还有其他一些参考文档。很痛苦,主要是东西太多,而我们目前又没有实际的问题,所以基本上是一边看一边忘!

恩。至少在这个阶段,先了解到这个程度就好。后面会随着学习的深入,有更多的深入知识,到时候根据需求再加进来。

2.2.2  odex介绍

再来看odex。odex是Optimized dex的简写,也就是优化后的dex文件。为什么要优化呢?主要还是为了提高Dalvik虚拟机的运行速度。但是odex不是简单的、通用的优化,而是在其优化过程中,依赖系统已经编译好的其他模块,简单点说:

  • 从Class文件到dex文件是针对Android平台的一种优化,是一种通用的优化。优化过程中,唯一的输入是Class文件。
  • odex文件就是dex文件具体在某个系统(不同手机,不同手机的OS,不同版本的OS等)上的优化。odex文件的优化依赖系统上的几个核心模块(由BOOTCLASSPATH环境变量给出,一般是/system/framework/下的jar包,尤其是core.jar)。我个人感觉odex的优化就好像是把中那些本来需要在执行过程中做的类校验、调用其他类函数时的解析等工作给提前处理了。

图10给出了图1所示示例代码得到的test.dex,然后利用dexopt得到test.odex,接着利用dexdump得到其内容,最后利用Beyond Compare比较这两个文件的差异。

图10中,绿色框中是test.dex的内容,红色框中是test.odex的内容,这也是两个文件的差异内容:

  • test.dex中,TestMain类仅仅是PUBLIC的,但test.odex则增加了VERIFIED和OPTIMIZED两项。VERIFIED是表示该类被校验过了,至于校验什么东西,以后再说。
  • 然后就是一些方法的不同了。优化后的odex文件,一些字节码指令变成了xxx-quick。比如图中最后一句代码对于的字节码中,未优化前invoke-virtual指令表示从method table指定项(图中是0002)里找到目标函数,而优化后的odex使用了invoke-virtual-quick表示从vtable中找到目标函数(图中是000b)。

vtable是虚表的意思,一般在OOP实现中用得很多。vtable一定比methodtable快么?那倒是有可能。我个人猜测:

  • method表应该是每个dex文件独有的,即它是基于dex文件的。
  • 根据odex文件的生成方法(后面会讲),我觉得vtable恐怕是把dex文件及依赖的类(比如Java基础类,如Object类等)放一起进行了处理,最终得到一张大的vtable。这个odex文件依赖的一些函数都放在vtable中。运行时直接调用指定位置的函数就好,不需要再解析了。以上仅是我的猜测。

1  http://mylifewithandroid.blogspot.com/2009/05/about-quick-method-invocation.html介绍了vtable的生成,大家可以看看

2  http://pallergabor.uw.hu/androidblog/dalvik_opcodes.html 详细描述了dex/odex指令的格式,大家有兴趣可以做参考。

(1)  odex文件的生成

前面曾经提到过,odex文件的生成依赖于BOOTCLASSPATH提供的系统核心库。以我们这个简单的例子而言,core.jar是必须的(java基础类大部分封装在core.jar中)。另外,core.jar对应的core.odex文件也需要。所有这些文件我都已经上传到示例代码仓库的javavmtest/odex-test目录下。然后执行dextest.sh脚本。此脚本内容如下:

#!/bin/sh
#在根目录下建立/data/dalvik-cache目录,这是因为odex往往是在机器上生成的,所有这些目录都是
#设备上才有。我们模拟一下罢了
sudo mkdir -p /data/dalvik-cache/
#core.dex文件名:这也是模拟了机器上的情况。系统将dex文件的绝对路径名换成了@来唯一标示
#一个dex文件。由于我在制作core.dex的时候,该core.jar包放在了/home/innost/workspace/my-projects/
#
CORE_TARGET_DEX="@"
CURRENT_PATH=`pwd`
#为了减少麻烦,我这里做了一个链接,将需要的dex文件链接到此目录下的core.dex
sudo ln -sf ${CURRENT_PATH}/core.dex /data/dalvik-cache/${CORE_TARGET_DEX}classes.dex
rm test.odex
#设置BOOTCLASSPATH变量
export BOOTCLASSPATH=${CURRENT_PATH}/core.jar
/home/innost/workspace/android-4.4.4/out/host/linux-x86/bin/dexopt --preopt ${CURRENT_PATH}/test.jar test.odex "m=y u=n" 
#删掉/data目录
sudo rm -rf /data

odex文件由dexopt生成,这个工具在SDK里没有,只能由源码生成。odex文件的生成有三种方式:

  • preopt:即OEM厂商(比如手机厂商),在制作镜像的时候,就把那些需要放到镜像文件里的jar包,APK等预先生成对应的odex文件,然后再把classes.dex文件从jar包和APK中去掉以节省文件体积。
  • installd:当一个apk安装的时候,PackageManagerService会调用installd的服务,将apk中的class.dex进行处理。当然,这种情况下,APK中的class.dex不会被剔除。
  • dalvik VM:preopt是厂商的行为,可做可不做。如果没有做的话,dalvik VM在加载一个dex文件的时候,会先生成odex。所以,dalvik VM实际上用得是odex文件。以后我们研究dalvik VM的时候会看到这部分内容。

实际上dex转odex是利用了dalvik vm,里边也会运行dalvik vm的相关方法。

2.3  小结

本节主要介绍了Class文件,以及在Android平台上的变种dex和odex文件。以标准角度来看,Class文件是由Java VM规范定义的,所以通用性更广。dex或者是odex只不过是规范在Android平台上的一种具体实现罢了,而且dex/odex在很多地方也需要遵守规范。因为dex文件的来源其实还是Class文件。

对于初学者而言,我建议了解Class文件的结构为主。另外,关于dex/odex的文件结构,除非有明确需求(比如要自己修改字节码等),否则以了解原理就可以。而且,将来我们看到dalvik vm的实际代码后,你会发现dex的文件内容还是会转换成代码里的那些你很熟悉的类型,数据结构。比如dex存储字符串是一种优化后的方法,但是到vm代码中,还不是只能用字符串来表示吗?

另外,你还会发现,Class、dex还是odex文件都存储了很多源码中的信息,比如类名、函数名、参数信息、成员变量信息等,而且直接用得是字符串。这和Native的二进制比起来,就容易看懂多了。

三、字节码的执行

下面我们来讲讲字节码的执行。很多人对Java字节码到底是怎么运行的比较好奇。Java字节码的运行和操作系统上(比如Linux)一个进程是如何执行其代码,从理论上说是一致的。只不过Java字节码的执行是JVM,而操作系统上一个进程其代码的执行是由CPU来完成。当然,现在JVM也可以把Java字节码直接转成机器码,然后交给CPU来执行。这样可以显著提高运行速度。

本节我们将介绍Android平台上Java字节码的执行。当然,我并不会具体分析每一行代码都是怎么执行的(比如函数参数的入栈,寄存器的使用),而只是想向大家介绍大体的流程,满足大家的好奇心。如果有更深次的学习需求,你就可以在本节基础上自行开展了!

下面所讲内容的源码全部位于AOSP源码/dalvik/vm/mterp/out目录下

mterp/out目录下有好些个源码文件,如图11所示:

这个目录中的文件就是不同平台上,Java字节码处理的代码。每一个平台包含一个汇编文件和一个C文件。

  • 前面讲过,Java字节码可以完全由JVM自己来执行,比如碰到一个new instance的字节码,就对应去调用内存分配函数。这种完全由JVM执行的情况,其对应代码位于InterpC-portable.cpp中。待会我们先分析它。
  • 对于ARM平台,则有InterpAsm-armXXX.S和对应的InterpC-armXXX.cpp。其中.S文件是汇编文件,而.CPP文件是对应的C++文件。二者要结合起来使用。
  • x86和mips平台与ARM平台类似。
  • 当CPU类型不属于ARM、x86或mips(也不采用纯解释方法),则通过InterpAsm-allstubs.S和interpAsm-allsubts.cpp来处理。

下面我们看对于new操作,portable、arm平台的处理。

3.1  portable的纯解释执行

在InterpC-portable.cpp中,有几处关键代码,先来看图12:

在这段代码中:

  • H(_op):这个宏定义了&&op_##_op这样的东西。op_#_op其实是一个标号(Label,和goto中的label是一个意思),而&&代表这个Label的地址[4]
  • HANDLE_OPCODE(_op):这个宏定义了一个标号op_##_op。
  • 在FINISH宏中,有一个goto *handleTable,这是portable模式下JVM执行Java字节码的关键。简单点说,portable模式下,每一种Java操作码(OPCode)都对应有一个处理逻辑(是一段代码,但不一定是函数),FINISH宏就是取出当前的操作码,然后跳转(goto)到对应的处理逻辑去处理它。

那么,handlerTable是怎么定义的呢?来看图13:

图13中:

  • dvmInterpretPortable是porttable模式下Java字节码的执行入口。也就是当执行Java字节码的时候(比如TestMain.class中的main函数时),都会调用这个函数。这里要强调一点,JVM执行的时候,除了Java字节码外,还有很多JVM自己的处理逻辑。比如分配内存时候对堆栈size的检查,看看是不是超标。
  • DEFINE_GOTO_TABLE则定义了操作码的标记。

那么,new操作符对应的goto label在哪里呢?来看图14:

你看,portable.cpp中通过HANDLE_OPCODE(OP_NEW_INSTANCE)定义了new操作符的处理逻辑。这段逻辑中,真正分配内存的操作是由红框的dvmAllocObject来处理的。

看到这里,你会发现JVM执行Java字节码还是比较容易理解的。其实对于arm等平台也是这样。

3.2  ARM平台上的执行

和portable下dvmInterpretPortable函数(Java字节码执行的入口函数)相对应的,其他模式下的入口函数是dvmMterpStd,其代码如图15所示:

dvmMterpStd中最重要的是dvmMterpStdRun,这个函数是由各平台对应的xxx.S汇编文件定义的。InterpAsm-armv7-a-neon.S对应的dvmMterpStdRun函数以及对new的处理逻辑如图16所示:

图16中:

  • dvmMterpStdRun也是通过GOTO_OPCODE调整到不同操作码处理逻辑的地方去执行。
  • new操作符对应的OP_NEW_INSTANCE处理也会调用dvmAllocObject来分配内存喔。

3.3  小结

这一节我们介绍了JVM是怎么执行Java字节码的,主要以揭秘性质为主,大家也以掌握原理为首要任务。其中,portable模式下,操作码是一条一条解释执行的。而具体CPU平台上,则是由相关汇编代码来处理。二者实际上大同小异。但是由CPU来执行,显然处理要快,比如对于+这种操作,用portable的解释执行当然比直接转换成机器指令来执行要慢很多。

到此,我们了解了Class文件结构,以及Java字节码到底是怎么执行的。下一步,我们就开始正式分析Dalvik虚拟机了。

四、Dalvik虚拟机启动

4.1  dalvik的启动

Android平台中,第一个虚拟机是通过app_process进程启动的,这个进程也就是大名鼎鼎的Zygote(含义是受精卵)。Zygote的启动我在《深入理解Android卷I》第四章深入理解Zygote中有详细分析,这里我们简单回顾下。图17所示为zygote启动的触发机制:

上述代码是位于init.rc中,当Linux天字号第一进程init启动后,将执行init.rc中的内容。此处的zygote的一个Service,对应的进程是/system/bin/app_process,后面的--zygote...等是该进程的参数。

zygote,也就是app_process,其源码位于frameworks/base/cmds/app_process里,源码比较少,主要是一个App_main.cpp。其main函数如下:

int main(int argc, char* const argv[])
{
    .......
    AppRuntime runtime;  //AppRuntime是关键数据结构
    const char* argv0 = argv[0];
    int i = runtime.addVmArguments(argc, argv);//添加参数,不重要

    // Parse runtime arguments.  Stop at first unrecognized option.
   .......
    if (zygote) {//我是zygote
        runtime.start("com.android.internal.os.ZygoteInit",
                startSystemServer ? "start-system-server" : "");
    } ......
}

runtime是核心对象,其类型是AppRuntime,是定义在app_process中的一个Class,它从AndroidRuntime派生。start函数就是AndroidRuntime中的,用于启动VM的入口。

4.1.1  AndroidRuntime start之一

start函数我们分两部分讲,第一部分如图18所示:

第一部分包含三个主要函数:

  • jni_invocation.Init:初始化JNI相关的几个重要函数。
  • startVm:注意,它传入了一个JNIEnv* env对象进去,当这个函数返回时,我们在JNI中天天见的JNIEnv对象就是这个东西。startVm是Dalvik VM的核心,该函数返回后,VM就基本就绪了。
  • startReg:注册Android平台中一些特有的JNI函数。
(1)  JniInvocation Init

该函数内容如图19所示:

该函数:

  • 通过dlopen加载libdvm.so。看来每个Java进程都会有这个东西。这可是dalvik vm的核心库。这个库有很多API,我个人觉得如果了解libdvm.so的话,应该能干很多事情。我们后续分析xposed就会看到。
  • 从libdvm.so中找到JNI_GetDefaultJavaVMInitArgs、JNI_CreateVM和JNI_GetCreateJavaVMs这三个函数指针。

所以,以后调用比如JNI_CreateVM_函数的时候,我们知道它的真实实现其实是位于libdvm.so中的JNI_CreateVM就好。

比较简单,Nothing more....

4.2  startVM之旅

startVM属于Android Runtime start函数的第一部分,不过该函数内容比较多,我们单独搞一大节来讲它!

startVM此函数前面一大段都是参数处理,所以对本文有意义的内容其实只有图20所示的部分:

核心内容还是在libdvm.so中的JNI_CreateVM函数中,这个函数定义在dalvik/vm/jni.cpp中。来看它!

4.2.1  JNI_CreateJavaVM

(1)  gDvm、JavaVMExt和JNIEnvExt

图21所示为此函数的主要代码:

图21中,首先扑面而来的就是Dalvik VM中的几个重量级数据结构:

  • gDvm,全局变量,数据类型为结构体DvmGlobals,该结构体是Dalvik的核心数据结构,几乎所有的重要成员,控制参数(比如堆栈大小,状态、已经加载的类信息)等都通过gDvm来管理。
  • JavaVMExt:JavaVM在JNI编程中代表虚拟机本身。在Dalvik中,这个虚拟机本身真正的数据类型是此处的JavaVMExt。由于JNI支持C和C++两种语言调用(对C而言,就是直接调用函数,对于C++而言,就是调用一个类的成员函数),所以JavaVM这个数据结构在C++里是一个类(如果定义了__cplusplus宏,就是_JavaVM类),在C里则是JNIInvokeInterface数据结构。
  • 同样,对于JNIEnvExt而言,当使用C++编译时候,它就是__JNIEnv类,使用C编译时就是JNINativeInterface。

图22所示为JavaVMExt和JNIEnvExt的内容:

图22中可知:

  • JavaVMExt有一个envList链表,该链表管理这一个Java进程中所有JNIEnv环境实体。JNIEnv环境和线程有关,什么样的线程会需要JNIEnv环境呢?所有从Java层调用JNI的线程以及从Native线程往调用Java函数的线程都需要创建一个JNIEnv。说白了,JNIEnv环境是Java和Native世界的桥梁。
  • JNIEnvExt提供的跨Java和Native的桥梁主要就是JNIEnv定义的那些函数,它们统一保存在JNINativeInterface数据结构体中,比如图中右下角红框中的NewGlobalRef、NewLocalRef等。
  • 注意,gDvm的funcTable变量指向了全局对象gInvokeInterface。该变量定义在dalvik/vm/jni.cpp中。

再来看gDvm的内容,它自己其实就是一大仓库,里边有很多成员变量,每个成员变量都有各自的用途。其内部如图23所示:

图23中:

  • gDvm的数据类型是DvmGlobals,里边存储了整个Dalvik虚拟机中相关的参数,成员变量。其中loadedClasses代表虚拟机加载的所有类信息。
  • classJavaLangClass指向一个类型为ClassObject的对象。ClassObject是Class信息在代码中的表示,其主要内容见图右上角,它包括类名信息、成员变量、函数(函数的代码表示是Method)等。classJavaLangClass代表的就是Java中最基础的java.lang.Class类。
  • ClassObject从Object类派生(C++中,struct其实就是class)

这里要特别说明虚拟机中对类唯一性的确定方法:

1  对我们而言,类的唯一性由包名+类名表示,比如java.lang.Class这个类,就是唯一的。但实际上,根据Java VM规范,类的唯一性由全路径类名+定义它的ClassLoader两者唯一确定。

2  对一个类的加载而言,ClassLoader有两种情况。一种是直接创建目标类,这种loader叫Define Loader(定义加载器)。另外一种情况是一个ClassLoader创建了Class,但它可以自己直接创建,也可以是委托给比如父加载器创建的,这种Loader叫Initiating Loader(初始加载器)。

3  类的唯一性是由全路径类名+定义加载器唯一决定。

下面来看JNIEnvExt的创建,这是由图21中的dvmCreateJNIEnv函数完成的。

(2)  dvmCreateJNIEnv

图21中的调用方法如下:

JNIEnvExt* pEnv = (JNIEnvExt*) dvmCreateJNIEnv(NULL);

该函数的相关代码如图24所示:

图24中,Dalvik虚拟机里JNI的所有函数都封装在gNativeInterface中。这个结构体包含了JNI定义的所有函数。注意,在使用sourceInsight的时候会有一些函数无法被解析。因为这些函数使用了类似图右下角的CALL_VIRTUAL宏方式定义。

我确认了下,应该所有函数的定义其实都在jni.cpp这一个文件里。

到此,我们为主线程创建和初始化了gDvm和JNI环境。下面来看dvmStartup。

4.2.2  dvmStartup:虚拟机创建的核心

去掉dvmStartup函数中一些判断代码后,该函数整个执行流程可由图25表示:

图25中,dvmStartup的执行从左到右。由于本章我只是想讨论dalvik是怎么执行的Java代码的,所以这里有一些函数(比如GC相关的,就不拟讨论)。

dvmStartup首先是解析参数,这些参数信息可能会传给gDvm相关的成员变量。解析参数是由setCommandLineDefaults和processOptions来完成的。具体代码就不看了,最终设置的几个重要的参数是:

  • gDvm.executionMode = kExecutionModeJit:如果定义的WITH_JIT宏,则执行模式是JIT模式。
  • gDvm.bootClassPathStr:由BOOTCLASSPATH环境变量提供。Nexus7 WiFi版4.4.4的值如图26所示。
  • gDvm.mainThreadStackSize = kDefaultStackSize。kDefaultStackSize值为16K,代表主线程的堆栈大小
  • gDvm.dexOptMode = OPTIMIZE_MODE_VERIFIED,用于控制odex操作,该参数表示只对verified的类进行odex。

图26为Nexus 7 Wi-Fi版4.4.4的BOOTCLASSPATH值:

图26可知,system/framework下几乎所有的jar包都被放在了BOOT CLASSPATH里。这意味这zygote进程加载了所有framework的包,这进一步意味着App也加载了所有framework的包.....。

下面来分析几个和本章目标相关的函数:

(1)  dvmThreadStartup

图27所示为dvmThreadStartup的一些关键代码和解释:

Thread是Dalvik中代表和管理一个线程的重要结构。注意,这里的Thread不简单是我们在Java层中的线程。在那里,我们只需要在线程里执行要干得活就可以了。而这里的Thread几乎模拟了一个CPU(或者说CPU上的一个核)是怎么执行代码的。比如Thread中为函数调用要设置和维护一个栈,还要要有一个变量指向当前正在执行的指令(大名鼎鼎的PC)。这一块我不想浪费时间介绍,有兴趣的童鞋们可以此为契机进行深入研究。

(2)  dvmInlineNativeStartup

dvmInlineNativeStartup主要是将一些常用的函数搞成inline似的。这里的inline,其实就是将某些Java函数搞成JNI。比如String类的charAt、compareTo函数等。相关代码如图28所示:

注意,在上面函数中,gDvm.inlineMethods只不过是分配了一个内存空间,该空间大小和gDvmInlineOpsTable一样。而gDvm.inlineMethods数组元素并未和gDvmInlineOpsTable挂上钩。当然,最终是会挂上的,但是不在这里。此处暂且不表。

(3)  dvmClassStartup

下面我们跳到dvmClassStartup,这个函数很重要。图29是其代码:

图29中:

  • 创建了一个Hash表,用来存储已经加载的类。
  • 创建了代表java.lang.Class和所有基础数据类型的Class信息。

下面来看processClassPath这个函数,它要加载所有的Boot Class,由于它涉及到类的加载,所以它也是本文的重点内容。先来看图30:

processClassPath主要是处理BOOTCLASSPATH,也就是图26中的那些位于system/framework/下的jar包。图31展示了prepareCpe的代码,该函数处理一个一个的文件:

prepareCpe倒是很简单:

  • 对于.jar/.zip/.apk结尾的文件,则调用dvmJarFileOpen进行处理。
  • 对于.dex结尾的文件则调用dvmRawDexFileOpen进行处理。
  • 处理成功后,则设置ClassPathEntry的kind为KCpeJar或者是KCpeDex,代表文件的类型是Jar还是Dex。并且设置cpe->ptr指针为对应的文件(jar文件则是JarFile,Dex文件这是RawDexFile)。存储它们的原因是因为后续要从这些文件中解析里边包含的信息。

这里我们看dvmJarFileOpen函数,如图32所示:

图32介绍了dvmJarFileOpen的主要内容,其中:

  • 打开jar中的classes.dex文件,然后判断有没有对应的odex文件。如果没有,就调用dexopt生成一个odex文件。文件后缀还是.dex,但是路径位于/data/dalvik-cache下。

到此dvmClassStartup就介绍完了。下面来看一个重要函数,dvmFindRequiredClassesAndMembers。

(4)  dvmFindRequiredClassesAndMembers

dvmFindRequiredClassesAndMembers初始化一些重要类和函数。其代码如图33所示:

dvmFindRequiredClassesAndMembers就是初始化一些类,函数,虚函数等等。我们重点关注它是怎么初始化的。一共有三个重要函数:

  • findClassNoInit:和Java层的findClass有关,涉及到JVM中如何加载一个Class。
  • dvmFindDirectMethodByDescriptor和dvmFindVirtualMethodByDescriptor:涉及到JVM中如何定位到一个方法。

重点是findClassNoInit,代码如图34所示:

图34中,有几个关键点:

  • dvmLookupClass:这是从gDvm的已加载Class Hash表里搜索,看看目标Class是否已经加载了。注意搜索时的匹配条件:前面也曾经说到过,除了类名要相同之外,该类的类加载器也必须一样。另外,当待搜索类的类加载器位于clazz的初始化加载类列表中的时候,即使两个类的定义ClassLoader不一样,也可以满足搜索条件。关于初始类加载器来确定唯一性,我没有在JVM规范中找到明确的说明。
  • loadClassFromDex:该函数将解析odex文件中的类信息。下面重点介绍它。
  • dvmAddClasstoHash:把这个新解析得到的Class加到Class Hash表里。
  • dvmLinkClass:解析这个Class的一些信息。比如,Class的基类是谁,该class实现了哪些接口。请大家回过头去看2.1节的图2 Class文件内部结构。一个Class的基类以及它实现的接口类信息都是通过对应的索引来间接指向基类Class以及接口类Class的。而dvmLinkClass处理完后,这些索引将由实际的ClassObject对象来替代。另外,dvmLinkClass将做一些校验,比如此Class的基类是final的话,那么这个Class就应该存在。

注意:我们在编写代码的时候,对于类的唯一性往往只知道全路径类名,很少关注ClassLoader的重要性。实际上,我之前曾经碰到过一个问题:通过两个不同ClassLoader加载的相同的Class居然不相等。当时很不明白为什么要这么设计, 直到我碰到一个真实事情:有一天我在等车,听见一个路人大声叫着“李志刚,李志刚”。我回头一看,以为他是在找人,结果发现他的宠物狗跑了出来。原来他的 宠物狗就叫李志刚。这就说明,两个具有相同名字的东西,实际上很能是完全不同的事物。所以,简单得以两个类是否同名来判断唯一性肯定是不行得了。

下面来看最重要的loadClassFromDex,这个函数其实就是把odex文件中的信息转换成ClassObject。我们来看它:loadClassFromDex代码如图34所示:

其中主要的加载函数是loadClassFromDex0,其代码如图35所示:

以上是loadClassFromDex0的第一部分内容,这这一块比较简单,也就是设置一些东西。下面看图36

图36中:

  • newClazz的基类和它所实现的接口类,在loadClassFromDex0中还只是一索引来标识。最后这些索引会在dvmLinkClass里转换并指向成真正的ClassObject。
  • 然后调用loadSFieldFromDex来解析类的静态成员信息。成员信息由数据结构DexFieldId表示,其实包含的那些信息

其实loadClassFromDex0后面的工作也类似,比如解析成员函数信息,成员变量信息等。我们直接看相关函数吧:

图37展示了解析成员变量和解析函数用的两个函数。

注意native函数的处理,此处是先用dvmResolveNativeMethod顶着。我们以后分析JNI的时候再来讨论它。

上面的findClassNoInit是用于搜索Class的,下面我们来看dvmFindDirectMethodByDescriptor函数,它是用来搜索方法的,代码如图38所示:

对compareMethodHelper好奇的读者,我在图40里展示了如何从dex文件中获取一个函数的返回值信息。

好像感觉我们一直和字符串在玩耍。

4.3  小结

说实话,讲到现在,其实虚拟机启动的流程差不多就完了。当然,本节所说的这个流程是很粗犷的,主要内容还是集中在Class的加载上,然后浮光掠影看了下一些重要的数据结构。Anyway,上述流程,我建议读者结合代码反复走几个来回。下面我们将开始介绍一些细节性的内容:

  • 第五章介绍类的初始化和加载。
  • 第六章介绍Java中的函数调用到底是怎么实现的。
  • 第七章介绍JNI的内容。

五、Class的加载和初始化

JVM中,一个Class首先被使用的时候会调用它的<clinit>函数。<clinit>函数是一个由编译器生成的函数,当类有static成员变量或者static语句块的时候,编译器就会为它生成这个函数。那么,我们要搞清楚这个函数在什么时候被调用,以什么样的方式被调用。

先来看一段示例代码,如图41所示:

示例代码中:

  • TestMain有一个静态成员变量another,其类型是TestAnother。初始值是NULL。
  • main函数中,构造了这个TestAnother对象。
  • TestAnother有一个静态成员变量testCLinit和static语句。
  • 最后一个图是执行结果。从其输出来看,main函数的“00000”先执行,然后执行的是TestAnother的static语句,最后是TestAnother的构造函数。

问题来了:TestAnother的<clinit>什么时候被调用?我一开始思考这个问题的时候:这个函数是编译器自动生成的,那么调用它的地方是不是也由编译器控制呢?

要确认这一点,只需要看dexdump的结果,如图42所示:

图42中:

  • 上图:由于TestMain也有静态成员变量,所以编译器为它生成了<clinit>函数。在它的<clinit>中,由于another变量赋值为null,所以没有触发another类的加载(不过,这个结论不是由图42得到的,而是由图41日志输出的顺序得到的)。
  • 下图:是TestMain的main函数。我们来看another对象的创建,首先是通过new-instance指令创建,然后通过invoke-direct调用了TestAnother的<init>函数。是的,你没看错,TestAnother的构造函数(也就是<init>)是明确被调用的,但是TestAnother的<clinit>调用之处却毫无踪迹。

当然,根据图41的日志输出,我们知道<clinit>是在TestAnother的构造函数之前调用的,那唯一有可能的地方会不会是new-instance呢?

5.1  new-instance

我们在3.1节portable的纯解释执行一节中提到过new-instance,下面我们将以portable为主要讲解对象来介绍。

其实,不管是portable还是arm、x86方式,最终都会变成机器指令来执行。相对arm、x86的汇编代码,portable是以C语言实现的Java字节码解释器,非常方便我们理解。

图43为new-instance指令对应的代码:

第六节会介绍portable模式下Java函数是如何执行的,所以这里大家先不用管HANDLE_OPCODE这样的宏是干什么用的。图43中:

  • 先调用dvmDexGetResolvedClass,看看目标类TestAnother是不是已经被解析过了。前面曾经提到说,一个类在初始化的时候可能会解析它所使用到的其他类。
  • 假设被引用的类没有解析过,则调用dvmResolveClass来加载目标类。
  • 目标类加载成功后,如果该类没有初始化过,则调用dvmInitClass进行初始化。

我们重点介绍dvmResolveClass和dvmInitClass。

5.1.1  dvmResolveClass分析

图44是dvmResolveClass的代码:

图44中:

  • 上图是dvmResolveClass的代码,其主要逻辑就是先得到目标类名(Lcom/test/TestAnother;)然后调用dvmFindClassNoInit来加载目标类。
  • 下图是dmvFindClassNoInit的代码,由于referrer的ClassLoader(也就是使用TestAnother类的TestMain类的ClassLoader)不为空,代码逻辑将走到findClassFromLoaderNoInit。注意,dvmFindSystemClassNoInit我们在4.2.2.4节将bootclass类解析的时候讲过。

图45是findClassFromLoaderNoInit的代码,出奇的简单:

代码真是简洁啊,居然调用java/lang/ClassLoader的loadClass函数来加载类。当然,dalvik中调用Java函数是通过dvmCallMethod来实现的。这个函数我们下一节再介绍。然后,我们把loader存储到目标clazz的初始加载loader链表中。初始加载链表在决定类唯一性的时候很有帮助(不记得初始加载器和定义加载器的同学们,请回顾图23后的说明和图33)。

Anyway,到此,目标类就算加载成功了。类加载成功到底意味这什么?前面讲过loadClassFromDex等函数,类加载成功意味着dalvik虚拟机从dex字节码文件中成功得到了一个代表该类的ClassObject对象,里边该填的信息在这里都填好了!

加载成功,下一步工作是初始化,来看下一节:

5.1.2  dvmInitClass分析

图46为dvmInitClass的代码:

终于,在dvmInitClass中,我们看到了<clinit>的执行。其他感觉没什么特别需要说的了。

再次强调,本章是整个虚拟机旅程中一次浮光掠影般的介绍,先让大家,包括我自己看看虚拟机是个什么样子,有一个粗略的认识即可。后续有打算搞一个完整的,严谨的,基于ART的虚拟机分析系列。

六、Java函数是怎么run起来的

JVM规范定义了JVM应该怎么执行一个函数,东西较碎,但和其他语言一样,无非是如下几个要点:

  • JVM在执行一个函数之前,它会首先分配一个栈帧(JVM中叫Frame),这个Frame其实就是一块内存,里边存储了参数,还预留了空间用来存储返回值,还有其他一些东西。
  • 函数执行时,从当前栈帧(每一个函数执行之前,JVM都会为它分配一个栈帧)获取参数等信息,然后执行,然后将返回值存储到当前栈帧。当前正在执行的函数叫current Method(当前方法)
  • 函数返回后,JVM回收当前栈帧。

函数执行肯定是在一个线程里来做的,栈帧则理所当然就会和某个线程相关联。我们先来看dalvik是怎么创建线程及对应栈的。

6.1  allocThread分析

Dalvik中,allocThread用于创建代表一个线程的线程对象,其代码如图47所示:

图47是dalvik虚拟机为一个线程创建代表对象的处理代码,其中,它为每个线程都创建了一个线程栈。线程栈大小默认为16KB,并设置了相关的栈顶和栈底指针,如图中右下角所示:

  • interpStackStart为栈顶,位于内存高位值。
  • interpStackEnd为栈底,位于内存地位。
  • 整个栈的内存起始位置为stackBottom。stackBottom和interpStackEnd还有一个768字节的保护区域。如果栈内容下压到这块区域,就认为出错了。

每个线程都分配16KB,会不会耗费内存呢?不会,这是因为mmap只是在内核里建立了一个内存映射项,这个项覆盖16KB内存。注意,它只是告诉kernel,这块区域最大能覆盖16KB内存。如果一直没有使用这块内存的话,那么内存并不会真正分配。所以,只有我们真正操作了这块内存,系统才会为它分配内存。

6.2  dvmCallMethod

dalvik中,如果需要调用某个函数,则会调用dvmCallMethod(嗯嗯?不对吧,Java字节码里的invoke-direct指令难道也是调用这个么?别急,待会再说invoke-direct的实现。)

dvmCallMethod第一步主要是调用callPrep准备栈帧,这是函数调用的关键一步,马上来看:

6.2.1  dvmPushInterpFrame

当调用一个Java函数时,JVM需要为它搞一个新的栈帧,图49展示了dvmPushInterpFrame的代码

图49中:

  • 一个栈帧的大小包括两个StackSaveArea和输入参数及函数内部本地变量(大小为method->registersSize*4)所需的空间。但是,在计算栈是否overflow的时候,会额外加上该函数内部调用其他函数时所传参数所占空间(大小为method->outsSize*4)
  • 这两个StackSaveArea,一个叫BreakSaveBlock,另外一个叫SaveBlock。其分布如图49中右下角位置所示。这两个SSA的作用,我们后面将看到。
  • self->interpSave.curFrame指向saveBlock的高地址。紧接其上的就是参数空间

1 注意:registersSize包括函数输入参数和函数内部本地变量的个数

2  dvmPushJNIFrame,这个函数是当Java要调用JNI函数时的压栈处理,该函数和dvmPushInterpFrame几乎一样,只是在计算所需栈空间时,没有加上outsSize*4,因为native函数所需栈是由Native自己控制的。此函数代码很简单,请童鞋们自己学习

好了,栈已经准备好了,我们看看函数到底怎么执行。

6.2.2  参数入栈

图48中dvmCallMethodV调用callPrep之后,有一段代码我们还没来得及展示,如图50所示:

参数入栈,您看明白了吗?

6.2.3  调用函数

接着看dvmCallMethodV调用函数部分,如图51所示

对于java函数,其处理逻辑由dvmInterpret完成,对于Native函数,则由对应的nativeFunc完成。JNI我们放到后面讲,先来处理dvmInterpret。如图52所示:

图52中:

  • self->interpSave.pc指向要指向函数的指令部分(method->insns)

下面我们来看dvmInterpretPortable的处理:

(1)  dvmInterpretPortable

dvmInterpretPortable位于dalvik/vm/mterp/out/InterpC-portable.cpp里,这个InterpC-portable.cpp是用工具生成的,将分散在其他地方的函数合并到最终这一个文件里。我们先来看该函数的第一段内容,如图53所示:

第一部分中,我们发现dvmInterpretPortable通过DEFINE_GOTO_TABLE定义了一个handlerTable[kNumPackedOpcodes]数组,这个数组里的元素通过H宏定义。H宏使用了&&操作符来获取某个goto label的位置。比如图中的H(OP_RETURN_VOID),展开这个宏后得到&&op_OP_RETURN_VOID,这表示op_OP_RETURN_VOID的位置。

那么,这个op_OP_RETURN_VOID标签是谁定义的呢?恩,图中的HANDLE_OPCODE宏定义的,展开后得到op_OP_RETURN_VOID:。

最后:

  • pc=self->interpSave.pc:将pc指向self->interpSave.pc,它是什么?回顾图52,原来这就是method->insns。也就是这个方法的第一个字节码指令。
  • fp=self->interpSave.curFrame:参看图50右边的示意图。

来看portable模式下Java字节码的处理,这也是最精妙的一部分,如图54所示:

请先认真看图54的内容,然后再看下面的总结,portable模式下:

  • FINISH(0):移动PC,然后获取对应指令的操作码到ins。根据ins获取该指令的操作码(注意,一条指令包含操作码和操作数),然后goto到该操作码对应的处理label处。
  • 在对应label处理逻辑处:从指令中提取参数,比如INST_A或INST_B。然后处理,然后再次调整PC,使得它能处理下一条指令。

好了,portable模式下dalvik如何运行java指令就是这样的,就是这么任性,就是这么简单。下面,我们来看Invoke-direct指令又是如何被解析然后执行的。

(2)  invoke-direct指令是如何被执行的

刚才你看到了portable模式下指令的执行,就是解析指令的操作码然后跳转到对应的label。假设我们现在碰到了invoke-direct指令,这是用来调用函数的。我们看看dvmInterpretPortable怎么处理它。一个图就可以了,如图55所示:

就是跳来跳去麻烦点,其实和dvmCallMethod一样一样。

(3)  函数返回

一切尽在图56。

函数返回后,还需要pop栈帧,代码在stack.cpp的dvmPopFrame中。此处略过不讨论了。

6.3  小结

这一节你真得要好好思考,函数调用,不论是Java、C/C++,python等等,都有这类似的处理:

  • 建立栈帧,参数入栈。
  • 跳转到对应函数的位置,native就是函数地址指针,Java这是goto label,转换成汇编还是地址指针。
  • 函数返回,pop栈帧。

这好像是程序设计的基础知识,这回你真正明白了吗?

七、JNI相关

关于JNI,我打算介绍下面几个内容:

  • Java层加载so库,so库中一般会注册相关JNI函数。
  • Java层调用native函数。

native库中,如果某个线程需要调用java函数,它会先创建一个JNIEnv环境,然后callXXMethod来调用Java层函数。这部分内容请大家自行研究吧....

把这几个步骤讲清楚的话,JNI内容就差不多了。

7.1  so加载和JNI函数注册

7.1.1  so文件搜索路径和so加载

APP中,如果要使用JNI的话,native函数必须封装在动态库里,Windows平台叫DLL,Linux平台叫so。然后,我们要在APP中通过System.loadLibrary方法把这个so加载进来。所以,入口是System的loadLibrary函数。相关代码如图57所示:

图57是System.loadLibrary的相关代码。这里主要介绍了so加载路径的问题:

  • 我们在应用里调用loadLibrary的时候系统默认会传入调用类的ClassLoader。如果有ClassLoader,则so必须由它加载。原因其实很简单,就是APP只能加载自己的so,而不能加载别的APP的so。这种做法和传统的linux平台上把so的搜索路径设置到LD_LIBRARY_PATH环境变量中有冲突,所以Android想出了这种办法。
  • 如果没有ClassLoader,则还是使用传统的LD_LIBRARY_PATH来搜索相关目录以加载so。

这里再明确解释下,loadLibrary只是指定了so文件的名字,而没有指定绝对路径。所以虚拟机得知道去哪个目录搜索这个文件。传统做法是搜索LD_LIBRARY_PATH环境变量所表明的文件夹(AOSP默认是/vendor/lib和/system/lib)这两个目录。但是我刚才讲,如果使用传统方法,APP A有so要加载的话,得把自己的路径加到LD_LIBRARY_PATH里去。比如LD_LIBRARY_PATH=/vendor/lib:/system/lib:/data/data/pkg-of-app-A/libs,这种方法将导致任何APP都可以加载A的so。

真正的加载由doLoad函数完成。这个函数相关的代码如图58所示:

没什么太多可说的,无非就是dlopen对应的so,然后调用JNI_OnLoad(如果该so定义了这个函数的话)。另外,dalvik虚拟机会保存自己加载的so项。

注意,图58里左边有两个笑脸,当然是很“阴险”的笑脸。什么意思呢?请童鞋们看看nativeLoad和它对应的Dalvik_java_lang_Runtime_nativeLoad函数。你会发现Runtime_nativeLoad的函数参数声明好奇怪,完全不符合JNI规范。并且,Runtime_nativeLoad的函数返回是void,但是Java中的nativeLoad却是有返回值的。怎么回事???此处不表,下文接着说。

7.1.2  JNI 函数主动注册和被动注册

(1)  调用RegisterNatives主动注册JNI函数

我们在JNI里,往往会自行注册java中native函数和native层对应函数的关系。这样,Java层调用native函数时候就会转到native层对应函数来执行。注册,是通过JNIEnv的RegisterNatives函数来完成的。我们来看看它的实现。如图59所示:

RegisterNatives里有几个比较重要的点:

  • 如果签名信息以!开头,则采用fastjni模式。这个玩意具体是什么,我们后面会讲。
  • Method的nativeFunc指向dvmCallJNIMethod,当java层调用native函数的时候会进入这个函数。而真正的native函数指针则存储在Method->insns中。我们知道insns代表一个函数的字节码.....。
(2)  被动注册

被动注册,也就是JNI里不调用RegisterNatives函数,而是让虚拟机根据一定规则来查找native函数的实现。一般的JNI教科书都是介绍被动注册,不过我从《深入理解Android卷1》开始就建议直接上主动注册方法。

dalvik中,当最开始加载类并解析其中的函数时,如果标记为native函数,则会把Method->nativeFunc设置为dvmResolveNativeMethod(请回头看图37)。我们来看这个函数的内容,如图60所示:

被动注册的方式是在该native函数第一次调用的时候被处理。童鞋们主要注意native函数的匹配规则。Anyway,不建议使用被动注册的方法,因为native层设置的函数名太长,搞起来很不方便。

7.2  调用Java native函数

6.2节专门讲过如何调用java函数,故事还得从dvmCallMethodV说起,如图61所示:

整个流程如下:

  • dvmCallMethodV发现目标函数是native的时候,就直接调用method->nativeFunc。当native函数已经解析过的时候,一般情况下该函数都指向dvmCallJNIMethod。如果这个native函数之前没有解析,则它指向dvmResolveNativeMethod。
  • dvmCallJNIMethod进行参数处理,然后调用dvmPlatformInvoke,这个函数一般由不同平台的汇编代码提供,大致工作流程也就是解析参数,压栈,然后调用method->insns指向的native层函数。

图62是X86平台上关于dvmPlatformInvoke注释:

也就是解析参数嘛,不多说了。和前面讲的Java准备栈帧类似,无非是用汇编写得罢了。

(1)  神秘得fastJni

fastJni,唉,可惜代码里有这个,但是好像没地方用。干啥的呢?还记得我们前面图58里的两个笑脸吗?

实话告诉大家,fastJni如果真正实现的话,可以加快JNI层函数的调用。为什么?我先给你看个东西,如图63所示:

图63需要好好解释下:

  • 首先,我们有两种类型的函数,一个是DalvikBridgeFunc,这个函数有四个参数。一个是DalvikNativeFunc,这个函数有两个参数。
  • dvmResolveNativeMethod或者是dvmCallJNIMethod都属于DalvikBridgeFunc类型。
  • 不过,如果是dalvik内部注册的native函数时候,比如Dalvik_java_lang_Runtime_nativeLoad这样的,它就属于dalvik内部注册的native函数,这个函数的类型就是DalvikNativeFunc。参考图61右上角。也就是说,Android为java.lang.Runtime.nativeLoad这个java层的native函数设置了一个native层的实现,这个实现就是Dalvik_java_lang_Runtime_nativeLoad。
  • 接着,这个函数被强制转换成DalvikBridgeFunc类型,并且设置到了Method->nativeFunc上。

这种做法会造成什么后果呢?

  • dvmCallMethodV发现自己调用的是native函数时候,直接调用Method->nativeFunc,也就是说,要么调用到dvmCallJNIMethod(或者是dvmResolveNativeMethod,姑且不论它)要么就直接调用到Dalvik_java_lang_Runtime_nativeLoad上了。

注意喔,这两个函数的参数一个是四个参数,一个是两个参数。不过注释中说了,给一个只有两个参数的函数传4个参数没有问题.....

等等,这么做的好处是什么?

  • 原来,dvmCallJNIMethod干了好多杂事,比如参数解析,参数入栈,然后才是通过dvmPlatformInvoke来调用真正的native层函数。而且还要对返回值进行处理。
  • fastJni模式下,直接调用对应的函数(比如Dalvik_java_lang_Runtime_nativeLoad),这样就没必要做什么参数入栈之类,也不用借助dvmPlatformInvoke再跳转了,肯定比dvmCallMethod省了不少时间。

当然,fastJni模式是有要求的,比如是静态,而且非synchronized函数。Anyway,目前这么高级的功能还是只有虚拟机自己用,没放开给应用层。

八  dalvik虚拟机小结

本篇是我第一次细致观察Android上Java虚拟机的实现,起因是想知道xposed的原理。我们下一篇会分析xposed的原理,其实蛮简单。因为xposed只涉及到了函数调用,hook之类的东西,没有虚拟机里什么内存管理,线程管理之类的。所以,我们这两篇文章都不会涉及内存管理,线程管理之类的高级玩意儿。

简单点说,本章介绍得和dalvik相关的内容还是比较好理解。希望各位先看看,有个感性认识,为将来我们搞更深入的研究而打点基础。

深入理解Android:卷II 图书简介: 《深入理解Android:卷2》是“深入理解Android”系列的第2本,第1本书上市后获得广大读者高度评价,在Android开发者社群内口口相传。本书不仅继承了第1本书的优点并改正了其在细微处存在的一些不足,而且还在写作的总体思想上进行了创新,更强调从系统设计者的角度去分析Android系统中各个模块内部的实现原理和工作机制。从具体内容上讲,本书的重点是AndroidFramework的Java层,对Java层涉及的核心模块和服务进行了深入而细致的分析。通过本书,读者不仅能对Android系统本身有更深入理解,而且还能掌握分析大型复杂源代码的能力。    《深入理解Android:卷2》一共8章:第1章介绍了阅读本书所需要做的准备工作,包括Android4.0源码的下载和编译、Eclipse环境的搭建,以及Android系统进程(system_process)的调试等;第2章对JavaBinder和MessageQueue的实现进行了深入分析;第3章仔细剖析了SystemServer的工作原理,这些服务包括EntropyService、DropboxManagerService、DiskStatsService、DeviceStorageMonitorService、SamplingProfilerService和ClipboardService;第4章对系统中负责Package信息查询和APK安装、卸载、更新等工作的服务PackageManagerService进行了详细分析;第5章则对Android系统中负责电源管理的核心服务PowerManagerService的原理进行了一番深入的分析;第6章以ActivityManagerService为分析重点,它的启动、Activity的创建和启动、BroadcastReceiver的工作原理、Android中的进程管理等内容展开了较为深入的研究;第7章对ContentProvider的创建和启动、SQLite、Cursorquery和close的实现等进行了深入分析;第8章以ContentService和AccountManagerService为分析对象,介绍了数据更新通知机制的实现,以及账户管理和数据同步等相关知识。 深入理解Android:卷II 图书目录: 第1章 搭建Android源码工作环境 1.1 Android系统架构 1.2 搭建开发环境 1.2.1 下载源码 1.2.2 编译源码 1.2.3 利用Eclipse调试system_process 1.3 本章小结 第2章 深入理解Java Binder和MessageQueue 2.1 概述 2.2 Java层中的Binder架构分析 2.2.1 Binder架构总览 2.2.2 初始化Java层Binder框架 2.2.3 addService实例分析 2.2.4 Java层Binder架构总结 2.3 心系两界的MessageQueue 2.3.1 MessageQueue的创建 2.3.2 提取消息 2.3.3 nativePollOnce函数分析 2.3.4 MessageQueue总结 2.4 本章小结 第3章 深入理解SystemServer 3.1 概述 3.2 SystemServer分析 3.2.1 main函数分析 3.2.2 Service群英会 3.3 EntropyService分析 3.4 DropBoxManagerService分析 3.4.1 DBMS构造函数分析 3.4.2 dropbox日志文件的添加 3.4.3 DBMS和settings数据库 3.5 DiskStatsService和DeviceStorageMonitorService分析 3.5.1 DiskStatsService分析 3.5.2 DeviceStorageManagerService分析 3.6 SamplingProfilerService分析 3.6.1 SamplingProfilerService构造函数分析 3.6.2 SamplingProfilerIntegration分析 3.7 ClipboardService分析 3.7.1 复制数据到剪贴板 3.7.2 从剪切板粘贴数据 3.7.3 CBS中的权限管理 3.8 本章小结 第4章 深入理解PackageManagerService 4.1 概述 4.2 初识PackageManagerService 4.3 PKMS的main函数分析 4.3.1 构造函数分析之前期准备工作 4.3.2 构造函数分析之扫描Package 4.3.3 构造函数分析之扫尾工作 4.3.4 PKMS构造函数总结 4.4 APK Installation分析 4.4.1 adb install分析 4.4.2 pm分析 4.4.3 installPackageWithVerification函数分析 4.4.4 APK 安装流程总结 4.4.5 Verification介绍 4.5 queryIntentActivities分析 4.5.1 Intent及IntentFilter介绍 4.5.2 Activity信息的管理 4.5.3 Intent 匹配查询分析 4.5.4 queryIntentActivities总结 4.6 installd及UserManager介绍 4.6.1 installd介绍 4.6.2 UserManager介绍 4.7 本章学习指导 4.8 本章小结 第5章 深入理解PowerManagerService 5.1 概述 5.2 初识PowerManagerService 5.2.1 PMS构造函数分析 5.2.2 init分析 5.2.3 systemReady分析 5.2.4 BootComplete处理 5.2.5 初识PowerManagerService总结 5.3 PMS WakeLock分析 5.3.1 WakeLock客户端分析 5.3.2 PMS acquireWakeLock分析 5.3.3 Power类及LightService类介绍 5.3.4 WakeLock总结 5.4 userActivity及Power按键处理分析 5.4.1 userActivity分析 5.4.2 Power按键处理分析 5.5 BatteryService及BatteryStatsService分析 5.5.1 BatteryService分析 5.5.2 BatteryStatsService分析 5.5.3 BatteryService及BatteryStatsService总结 5.6 本章学习指导 5.7 本章小结 第6章 深入理解ActivityManagerService 6.1 概述 6.2 初识ActivityManagerService 6.2.1 ActivityManagerService的main函数分析 6.2.2 AMS的 setSystemProcess分析 6.2.3 AMS的 installSystemProviders函数分析 6.2.4 AMS的 systemReady分析 6.2.5 初识ActivityManagerService总结 6.3 startActivity分析 6.3.1 从am说起 6.3.2 AMS的startActivityAndWait函数分析 6.3.3 startActivityLocked分析 6.4 Broadcast和BroadcastReceiver分析 6.4.1 registerReceiver流程分析 6.4.2 sendBroadcast流程分析 6.4.3 BROADCAST_INTENT_MSG消息处理函数 6.4.4 应用进程处理广播分析 6.4.5 广播处理总结 6.5 startService之按图索骥 6.5.1 Service知识介绍 6.5.2 startService流程图 6.6 AMS中的进程管理 6.6.1 Linux进程管理介绍 6.6.2 关于Android中的进程管理的介绍 6.6.3 AMS进程管理函数分析 6.6.4 AMS进程管理总结 6.7 App的 Crash处理 6.7.1 应用进程的Crash处理 6.7.2 AMS的handleApplicationCrash分析 6.7.3 AppDeathRecipient binderDied分析 6.7.4 App的Crash处理总结 6.8 本章学习指导 6.9 本章小结 第7章 深入理解ContentProvider 7.1 概述 7.2 MediaProvider的启动及创建 7.2.1 Context的getContentResolver函数分析 7.2.2 MediaStore.Image.Media的query函数分析 7.2.3 MediaProvider的启动及创建总结 7.3 SQLite创建数据库分析 7.3.1 SQLite及SQLiteDatabase家族 7.3.2 MediaProvider创建数据库分析 7.3.3 SQLiteDatabase创建数据库的分析总结 7.4 Cursor 的query函数的实现分析 7.4.1 提取query关键点 7.4.2 MediaProvider 的query分析 7.4.3 query关键点分析 7.4.4 Cursor query实现分析总结 7.5 Cursor close函数实现分析 7.5.1 客户端close的分析 7.5.2 服务端close的分析 7.5.3 finalize函数分析 7.5.4 Cursor close函数总结 7.6 ContentResolver openAssetFileDescriptor函数分析 7.6.1 openAssetFileDescriptor之客户端调用分析 7.6.2 ContentProvider的 openTypedAssetFile函数分析 7.6.3 跨进程传递文件描述符的探讨 7.6.4 openAssetFileDescriptor函数分析总结 7.7 本章学习指导 7.8 本章小结 第8章 深入理解ContentService和AccountManagerService 8.1 概述 8.2 数据更新通知机制分析 8.2.1 初识ContentService 8.2.2 ContentResovler 的registerContentObserver分析 8.2.3 ContentResolver的 notifyChange分析 8.2.4 数据更新通知机制总结和深入探讨 8.3 AccountManagerService分析 8.3.1 初识AccountManagerService 8.3.2 AccountManager addAccount分析 8.3.3 AccountManagerService的分析总结 8.4 数据同步管理SyncManager分析 8.4.1 初识SyncManager 8.4.2 ContentResolver 的requestSync分析 8.4.3 数据同步管理SyncManager分析总结 8.5 本章学习指导 8.6 本章小结 “深入理解Android”系列书籍的规划路线图
深入理解Android:Telephony原理剖析与最佳实践》是“深入理解Android”系列的第3本,本书从源代码角度深入解析了Android Telephony的架构设计与实现原理,深刻揭示了Android系统的通信机制。对于Android应用开发工程师和系统工程师而言,本书都是难得的研究和学习资料。 全书共13章,分为五部分:第一部分(1~3章),首先介绍了智能手机的系统结构、Android系统的架构、Telephony框架的结构,然后详细介绍了Android源代码编译环境和阅读环境的搭建方法,以及阅读《深入理解Android:Telephony原理剖析与最佳实践》要做的技术准备;第二部分(4~6章),对Android的通话功能进行了深入的分析,包括对通话流程的分析、对主动拨号和来电流程的分析、对通话应用机制的分析,以及对手机通信功能在框架层和应用层中的实现机制的分析;第三部分(7~9章),对Android的通信功能进行了深入的分析,包括对网络服务状态的运行机制的分析、对Android手机上网的实现机制的分析,以及对短息发送和接收流程的分析;第四部分(10~12章),对Android RIL的工作机制进行了深入的分析,包括对框架层中的RILJ运行机制的分析、对RILC系统结构及LibRIL运行机制的分析,以及对Reference-RIL框架的原理的分析;第五部分(13章),分析了Telephony模块所提供的系统服务,包括系统服务的注册入口以及调用系统服务接口的实例。
CruiseYoung提供的带有详细书签的电子书籍目录 http://blog.csdn.net/fksec/article/details/7888251 深入理解Android:卷I(51CTO网站“2011年度最受读者喜爱的原创IT技术图书”) 基本信息 作者: 邓凡平 出版社:机械工业出版社 ISBN:9787111357629 上架时间:2011-9-13 出版日期:2011 年9月 开本:16开 页码:488 版次:1-1 编辑推荐   结合实际应用开发需求,以情景分析的方式有针对性地对Android的源代码进行了十分详尽的剖析,深刻揭示Android系统的工作原理    机锋网、51CTO、开源中国社区等专业技术网站一致鼎力推荐 内容简介   《深入理解android:卷1》是一本以情景方式对android的源代码进行深入分析的书。内容广泛,以对framework层的分析为主,兼顾native层和application层;分析深入,每一部分源代 码的分析都力求透彻;针对性强,注重实际应用开发需求,书中所涵盖的知识点都是android应用开发者和系统开发者需要重点掌握的。    全书共10章,第1章介绍了阅读本书所需要做的准备工作,主要包括对android系统架构和源码阅读方法的介绍;第2章通过对android系统中的mediascanner进行分析,详细讲解了 android中十分重要的jni技术;第3章分析了init进程,揭示了通过解析init.rc来启动zygote以及属性服务的工作原理;第4章分析了zygote、systemserver等进程的工作机制,同时还讨论了 android的启动速度、虚拟机heapsize的大小调整、watchdog工作原理等问题;第5章讲解了android系统中常用的类,包括sp、wp、refbase、thread等类,同步类,以及java中的handler类和 looper类,掌握这些类的知识后方能在后续的代码分析中做到游刃有余;第6章以mediaserver为切入点,对android中极为重要的binder进行了较为全面的分析,深刻揭示了其本质。第7章对 audio系统进行了深入的分析,尤其是audiotrack、audioflinger和audiopolicyservice等的工作原理。第8章深入讲解了surface系统的实现原理,分析了surface与activity之间以及surface 与surfaceflinger之间的关系、surfaceflinger的工作原理、surface系统中的帧数据传输以及layerbuffer的工作流程。第9章对vold和rild的原理和机制进行了深入的分析,同时还探讨了 phone设计优化的问题;第10章分析了多媒体系统中mediascanner的工作原理。    本书适合有一定基础的android应用开发工程师和系统工程师阅读。通过对本书的学习,大家将能更深刻地理解android系统,从而自如应对实际开发中遇到的难题。 作译者   邓凡平,资深Android开发工程师,热衷于Android源代码的研究,对Android的架构设计和实现原理有非常深刻的认识和理解,应用开发经验也十分丰富。目前就职于国内一家领先的 Android企业,负责Framework的开发和维护。乐于分享,活跃于CSDN等专业社区,撰写的Android Framework源码的系列文章深受读者欢迎。此外,他对Linux内核、C/C++/Python相关的技术 ,以及高性能网络服务器和多核并行开发等也有一定的研究。 目录 封面 -17 封底 489 扉页 -16 版权 -15 推荐序 -14 前言 -12 致谢 -9 目录 -7 第1章 阅读前的准备工作 1 1.1 系统架构 2 1.1.1 Android系统架构 2 1.1.2 本书的架构 3 1.2 搭建开发环境 4 1.2.1 下载源码 4 1.2.2 编译源码 6 1.3 工具介绍 8 1.3.1 Source Insight介绍 8 1.3.3 Busybox的使用 11 1.4 本章小结 12 第2章 深入理解JNI 13 2.1 JNI概述 14 2.2 学习JNI的实例:MediaScanner 15 2.3 Java层的MediaScanner分析 16 2.3.1 加载JNI库 16 2.3.2 Java的native函数和总结 17 2.4 JNI层MediaScanner的分析 17 2.4.1 注册JNI函数 18 2.4.2 数据类型转换 22 2.4.3 JNIEnv介绍 24 2.4.4 通过JNIEnv操作jobject 25 2.4.5 jstring介绍 27 2.4.6 JNI类型签名介绍 28 2.4.7 垃圾回收 29 2.4.8 JNI中的异常处理 32 2.5 本章小结 32 第3章 深入理解init 33 3.1 概述 34 3.2 init分析 34 3.2.1 解析配置文件 38 3.2.2 解析service 42 3.2.3 init控制service 48 3.2.4 属性服务 52 3.3 本章小结 60 第4章 深入理解zygote 61 4.1 概述 62 4.2 zygote分析 62 4.2.1 AppRuntime分析 63 4.2.2 Welcome to Java World 68 4.2.3 关于zygote的总结 74 4.3 SystemServer分析 74 4.3.1 SystemServer的诞生 74 4.3.2 SystemServer的重要使命 77 4.3.3 关于SystemServer的总结 83 4.4 zygote的分裂 84 4.4.1 ActivityManagerService发送请求 84 4.4.2 有求必应之响应请求 86 4.4.3 关于zygote分裂的总结 88 4.5 拓展思考 88 4.5.1 虚拟机heapsize的限制 88 4.5.2 开机速度优化 89 4.5.3 Watchdog分析 90 4.6 本章小结 93 第5章 深入理解常见类 95 5.1 概述 96 5.2 以“三板斧”揭秘RefBase、 sp和WP 96 5.2.1 第一板斧&mdash;&mdash;初识影子对象 96 5.2.2 第二板斧&mdash;&mdash;由弱生强 103 5.2.3 第三板斧&mdash;&mdash;破解生死魔咒 106 5.2.4 轻量级的引用计数控制类LightRefBase 108 5.2.5 题外话&mdash;&mdash;三板斧的来历 109 5.3 Thread类及常用同步类分析 109 5.3.1 一个变量引发的思考 109 5.3.2 常用同步类 114 5.4 Looper和Handler类分析 121 5.4.1 Looper类分析 122 5.4.2 Handler分析 124 5.4.3 Looper和Handler的同步关系 127 5.4.4 HandlerThread介绍 129 5.5 本章小结 129 第6章 深入理解Binder 130 6.1 概述 131 6.2 庖丁解MediaServer 132 6.2.1 MediaServer的入口函数 132 6.2.2 独一无二的ProcessState 133 6.2.3 时空穿越魔术&mdash;&mdash;defaultServiceManager 134 6.2.4 注册MediaPlayerService 142 6.2.5 秋风扫落叶&mdash;&mdash;StartThread Pool和join Thread Pool分析 149 6.2.6 你彻底明白了吗 152 6.3 服务总管ServiceManager 152 6.3.1 ServiceManager的原理 152 6.3.2 服务的注册 155 6.3.3 ServiceManager存在的意义 158 6.4 MediaPlayerService和它的Client 158 6.4.1 查询ServiceManager 158 6.4.2 子承父业 159 6.5 拓展思考 162 6.5.1 Binder和线程的关系 162 6.5.2 有人情味的讣告 163 6.5.3 匿名Service 165 6.6 学以致用 166 6.6.1 纯Native的Service 166 6.6.2 扶得起的“阿斗”(aidl) 169 6.7 本章小结 172 第7章 深入理解Audio系统 173 7.1 概述 174 7.2 AudioTrack的破解 174 7.2.1 用例介绍 174 7.2.2 AudioTrack (Java空间)分析 179 7.2.3 AudioTrack (Native空间)分析 188 7.2.4 关于AudioTrack的总结 200 7.3 AudioFlinger的破解 200 7.3.1 AudioFlinger的诞生 200 7.3.2 通过流程分析AudioFlinger 204 7.3.3 audio track cblk t分析 230 7.3.4 关于AudioFlinger的总结 234 7.4 AudioPolicyService的破解 234 7.4.1 AudioPolicyService的创建 235 7.4.2 重回AudioTrack 245 7.4.3 声音路由切换实例分析 251 7.4.4 关于AudioPolicy的总结 262 7.5 拓展思考 262 7.5.1 DuplicatingThread破解 262 7.5.2 题外话 270 7.6 本章小结 272 第8章 深入理解Surface系统 273 8.1 概述 275 8.2 一个Activity的显示 275 8.2.1 Activity的创建 275 8.2.2 Activity的UI绘制 294 8.2.3 关于Activity的总结 296 8.3 初识Surface 297 8.3.1 和Surface有关的流程总结 297 8.3.2 Surface之乾坤大挪移 298 8.3.3 乾坤大挪移的JNI层分析 303 8.3.4 Surface和画图 307 8.3.5 初识Surface小结 309 8.4 深入分析Surface 310 8.4.1 与Surface相关的基础知识介绍 310 8.4.2 SurfaceComposerClient分析 315 8.4.3 SurfaceControl分析 320 8.4.4 writeToParcel和Surface对象的创建 331 8.4.5 lockCanvas和unlockCanvasAndPost分析 335 8.4.6 GraphicBuffer介绍 344 8.4.7 深入分析Surface的总结 353 8.5 SurfaceFlinger分析 353 8.5.1 SurfaceFlinger的诞生 354 8.5.2 SF工作线程分析 359 8.5.3 Transaction分析 368 8.5.4 关于SurfaceFlinger的总结 376 8.6 拓展思考 377 8.6.1 Surface系统的CB对象分析 377 8.6.2 ViewRoot的你问我答 384 8.6.3 LayerBuffer分析 385 8.7 本章小结 394 第9章 深入理解Vold和Rild 395 9.1 概述 396 9.2 Vold的原理与机制分析 396 9.2.1 Netlink和Uevent介绍 397 9.2.2 初识Vold 399 9.2.3 NetlinkManager模块分析 400 9.2.4 VolumeManager模块分析 408 9.2.5 CommandListener模块分析 414 9.2.6 Vold实例分析 417 9.2.7 关于Vold的总结 428 9.3 Rild的原理与机制分析 428 9.3.1 初识Rild 430 9.3.2 RIL_startEventLoop分析 432 9.3.3 RIL Init分析 437 9.3.4 RIL_ register分析 444 9.3.5 关于Rild main函数的总结 447 9.3.6 Rild实例分析 447 9.3.7 关于Rild的总结 459 9.4 拓展思考 459 9.4.1 嵌入式系统的存储知识介绍 459 9.4.2 Rild和Phone的改进探讨 462 9.5 本章小结 463 第10章 深入理解MediaScanner 464 10.1 概述 465 10.2 android.process.media分析 465 10.2.1 MSR模块分析 466 10.2.2 MSS模块分析 467 10.2.3 android.process.media媒体扫描工作的流程总结 471 10.3 MediaScanner分析 472 10.3.1 Java层分析 472 10.3.2 JNI层分析 476 10.3.3 PVMediaScanner分析 479 10.3.4 关于MediaScanner的总结 485 10.4 拓展思考 486 10.4.1 MediaScannerConnection介绍 486 10.4.2 我问你答 487 10.5 本章小结 488 前言   虽然前言位于书的最前面,但往往是最后才完成的。至今,本书的撰写工作算是基本完成了,在书稿付梓之前,心中却有些许忐忑和不安,因为拙著可能会存在Bug。为此,我先为书中可 能存在的Bug将给大家带来的麻烦致以真诚的歉意。另外,如果大家发现本书存在纰漏或有必要进一步探讨的地方,请发邮件给我(fanping.deng@gmail.com),我会尽快回复。非常乐意与大 家交流。      本书主要内容   全书一共10章,其中一些重要章节中还设置了“拓展思考”部分。这10章的主要内容是:   第1章介绍了阅读本书所需要做的一些准备工作,包括对Android整个系统架构的认识,以及Android开发环境和源码阅读环境的搭建等。注意,本书分析的源码是Android2.2。   第2章通过Android源码中的一处实例深入地介绍了JNI技术。   第3章围绕init进程,介绍了如何解析init.rc以启动Zygote和属性服务(property service)的工作原理。   第4章剖析了zygote和system_server进程的工作原理。本章的拓展思考部分讨论了Andorid的启动速度、虚拟机heapsize的大小调整问题以及“看门狗”的工作原理。   第5章讲解了Android源码中常用的类,如sp、wp、RefBase、Thread类、同步类、Java中的Handler类以及Looper类。这些类都是Android中最常用和最基本的,只有掌握这些类的知识,才 能在分析后续的代码时游刃有余。   第6章以MediaServer为切入点,对Binder进行了较为全面的分析。本章拓展思考部分讨论了与Binder有关的三个问题,它们分别是Binder和线程的关系、死亡通知以及匿名Service。笔者 希望,通过本章的学习,大家能更深入地认识Binder的本质。   第7章阐述了Audio系统中的三位重要成员AudioTrack、AudioFlinger和AudioPolicyService的工作原理。本章拓展思考部分分析了AudioFlinger中DuplicatingThread的工作原理,并且和 读者一道探讨了单元测试、ALSA、Desktop check等问题。通过对本章的学习,相信读者会对Audio系统有更深的理解。   第8章以Surface系统为主,分析了Activity和Surface的关系、Surface和SurfaceFlinger的关系以及SurfaceFlinger的工作原理。本章的拓展思考部分分析了Surface系统中数据传输控制 对象的工作原理、有关ViewRoot的一些疑问,最后讲解了LayerBuffer的工作流程。这是全书中难度较大的一章,建议大家反复阅读和思考,这样才能进一步深入理解Surface系统。   第9章分析了Vold和Rild,其中Vold负责Android平台中外部存储设备的管理,而Rild负责与射频通信有关的工作。本章的拓展思考部分介绍了嵌入式系统中与存储有关的知识,还探讨了 Rild和Phone设计优化方面的问题。   第10章分析了多媒体系统中MediaScanner的工作原理。在本章的拓展思考部分,笔者提出了几个问题,旨在激发读者深入思考和学习Android的欲望。      本书特色   笔者认为,本书最大的特点在于,较全面、系统、深入地讲解了Android系统中的几大重要组成部分的工作原理,旨在通过直接剖析源代码的方式,引领读者一步步深入于诸如Binder、 Zygote、Audio、Surface、Vold、Rild等模块的内部,去理解它们是如何实现的,以及如何工作的。笔者根据研究Android代码的心得,在本书中尝试性地采用了精简流程、逐个击破的方法进 行讲解,希望这样做能帮助读者更快、更准确地把握各模块的工作流程及其本质。本书大部分章节中都专门撰写了“拓展思路”的内容,希望这部分内容能激发读者对Android代码进行深入研 究的热情。      本书面向的读者   (1)Android应用开发工程师 .  对于Android应用开发工程师而言,本书中关于Binder,以及sp、wp、Handler和Looper等常用类的分析或许能帮助你迅速适应Android平台上的开发工作。   (2)Android系统开发工程师   Android系统开发工程师常常需要深入理解系统的运转过程,而本书所涉及的内容可能正是他们在工作和学习中最想了解的。那些对具体模块(如Audio系统和Surface系统)感兴趣的读者 也可以直接阅读相关章节的内容。   这里有必要提醒一下,要阅读此书,应具有C++的基本知识,因为本书的大部分内容都集中在了Native层。      如何阅读本书   本书是在分析Android源码的基础上展开的,而源码文件所在的路径一般都很长,例如,文件AndroidRuntime.cpp的真实路径就是framework/base/core/jni/AndroidRuntime.cpp。为了书 写方便起见,我们在各章节开头把该章所涉及的源码路径全部都列出来了,而在具体分析源码时,则只列出该源码的文件名。   下面就是一个示例:   [--]AndroidRuntime.cpp]   //这里是源码分析和一些注释。   如有一些需要特别说明的地方,则会用下面的格式表示:   [--]AndroidRuntime.cpp::特别说明]   特别说明可帮助读者找到源码中的对应位置。   另外,本书在描述类之间的关系以及在函数调用流程上使用了UML的静态类图以及序列图。UML是一个强大的工具,但它的建模规范过于烦琐,为更简单清晰地描述事情的本质,本书并未 完全遵循UML的建模规范。   本书所使用的UML图都比较简单,读者大可不必花费时间专门学习UML。   本书的编写顺序,其实应该是6、5、4、7、8、9、10、2、3、1章,但出于逻辑连贯性的考虑,还是建议读者按本书的顺序阅读。其中,第2、5、6章分别讲述了JNI、Android常用类以及 Binder系统,这些都是基础知识,我们有必要完全掌握。其他部分的内容都是针对单个模块的,例如Zygote、Audio、Surface、MediaScanner等,读者可各取所需,分别对其进行研究。      致谢   首先要感谢杨福川编辑。本书最初的内容来自我的博客,但博客里的文章都没有图,格式也较混乱。是杨编辑最先鼓励我将这些博文整理修改成册,所以我对杨福川编辑的眼光佩服得五 体投地。在他的同事杨绣国和白宇的帮助下,我最终才将博客中那些杂乱的文章撰成了今天这本图文并茂、格式工整的书籍。   其次要感谢我的妻子。为写成此书,我几乎将周末所有的时间都花在了工作中,而长时间在生活上对妻子不闻不问。对丈夫呆若木鸡式的冷淡,妻子却给予了最大的宽容。另外,我的岳 父母和我的父母亲都给予了我最无私的帮助,他们都是平凡而伟大的父母亲。还有我和妻子的亲戚们,他们的宽厚和善良时刻感动着我。   在IT职业的道路上,非常感念前东家中科大洋公司的领导和同事们,他们是邓伟先生、刘运红先生、王宁先生等。当初,如果没有他们宽容的接纳和细心的指导,现在我不可能成为一名 合格的程序员。   非常感谢我现在供职的单位中科创达公司。在这里工作,我常有这样一种感慨:不是所有人都能自己开公司创业的,而又有多少人能够有机会和一个优秀的创业公司一起成长、一起发展 呢?创达开明的领导、睿智而富有激情的工作伙伴正是孕育本书的沃土。公司领导赵鸿飞先生、吴安华女士等人更是给予了我最大的肯定和鼓励。   这里要特别提及的是,我的大学同窗,即为本书作序的邓必山先生。如果没有他的推荐,凭自己那份简陋、单薄的简历,是根本无法与Android亲密接触的。另外,他还曾在技术和个人发 展上给予过我很多的指导,对此,我将永志不忘!   谢谢那些共享Android知识的网友们!没有大家前期点滴的奉献,或许我至今还在琢磨着某段代码呢。   最后应感谢的是肯花费时间和精力阅读本书的读者,你们的意见和建议将会是我获得的巨大的精神财富!      邓凡平   2011年6月于北京    序言   近两年来,IT行业的最热点聚焦到了移动互联网上。PC时代,WINTEL联盟成就了英特尔和微软各自的霸业。移动互联网时代,谁将上演新的传奇?新生的Android当年仅用短短一年多的时 间就跻身全球智能操作系统的三甲行列。在北美市场,如今Android已经超过iOS和黑莓系统成为老大!Android势不可挡,ARM+Android组合的前景一片光明,越来越多的从业者加入了Android 行列!   与带给人们良好用户体验的iOS不一样的是,Android是一个开放的系统,其所有代码都是开源的。因此,对于开发者而言,不仅可以做到知其然,更可以做到知其所以然!   然而,要想知道其所以然,并不是一件简单的事情。回想当初,我开始接触Android的时候,除了Android源码外,其他资料甚少。Android是基于Linux的完整操作系统,其代码量让人望 而生畏。可以想象,在没有指导的情况下一头扎进操作系统庞大的代码中是一件让人多么痛苦的事情。时间过得很快,Android生态链已经得到了充分的发展。现在市场上的Android资料已经 开始泛滥,书籍已经数不胜数。然而,绝大部分书籍只限于讲解Android应用的开发(拜Android应用API所赐),没有深入到系统级的探讨,极少的所谓提供Android深入指导的资料也只是浅 尝辄止。如果想深入了解Android系统,只有华山一条路:自己看Android源代码!   正是因为如此,当初凡平告诉我他要系统地整理其深入钻研Android源代码的心得时,我表示了强烈的赞同。这是一件极少有人做过的事情,这件事情将给已经或即将跨入Android世界的 同仁们极大的帮助!这本书里,作者以代码框架为主线,用循序渐进的方式将框架中的关键点一一剖开,从而给读者一个架构清楚、细节完善的立体展现。另外,凡平还会用他的幽默给正在 啃枯燥代码的您带来不少笑意和轻松。毫无疑问,如果您想深入了解Android系统,这本书就是您进入Android神秘世界的钥匙。   如果您看准了移动互联网的前景,想深入理解Android,那就让这本书指导您前进吧!      邓必山   2011年6月于北京    媒体评   作者是Thundersoft多媒体组的牛人,技术精深,乐于分享,对Android系统有真正的理解。《深入理解Android:卷1》内容给力,语言生动,全书没有一句废话,各章中的“拓展思考” 尤为精彩,体现了作者对Android实现原理的深入理解和批判性思考。为什么Android的短信群发很慢?为什么拔出SD卡时有的程序会退出?读者都能从本书中找到诸如此类的各种实际问题的 答案。更重要的是,读者能够对Android的整个体系有一个全新的理解。如果你通读了这本书,请一定投一份简历给我们。&mdash;&mdash;Thundersoft(中科创达软件科技(北京)有限公司)      对于Android开发工程师而言,本书不可多得,分析透彻深入,针对性极强。Android系统本身极为庞大,如果要对整个系统进行面面俱到且细致入微地分析,恐怕不是一两本书能完成的 。本书从开发者的实际需求出发,有针对性地对Android系统中的重要知识点和功能模块的源代码实现进行了剖析,这样既能帮助开发者解决实际问题,又能使分析深入透彻,而不是停留于表 面。强烈推荐!&mdash;&mdash;机锋网(http://www.gfan.com/)      这本书非常实用,绝不是枯燥的源代码分析,是深入理解Android工作机制和实现原理的一本好书。为什么说它实用呢?因为它的最终目的并不是停留着源代码分析上,而是要帮助开发者 解决实际问题,于是所有知识点的分析和讲解都是从开发者的实际需求出发的。与一般的源代码分析的书相比较而言,本书在语言上多了几分幽默,更加生动易懂。更重要的是,本书的分析 十分深入,探讨了Android相关功能模块的本质。&mdash;&mdash;51CTO移动开发频道(http://mobile.51cto.com/)      随着Android 系统越来越流行,Android应用的需求也在不断变化,对于开发者而言,深入理解Android系统原理显得越来越重要。目前市面上Android 开发相关的图书已经很多,但真正 能够系统、深入地讲解Android系统原理的书还乏善可陈。这本书的出版恰逢其时,该书同时兼备深度和广度,以循序渐进的方式,优雅的语言,深入分析到各个模块的源码与原理。另外,它 启发性的讲解方式,更有助于读者的学习和思考。&mdash;&mdash;开源中国社区(http://www.oschina.net/)   
©️2021 CSDN 皮肤主题: 书香水墨 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值