深入解析Android 虚拟机

978-7-115-42353-5
作者: 钟世礼
译者:
编辑: 张涛

图书目录:

详情

本书循序渐进的讲解了Android虚拟机系统的基本知识,并剖析了其整个内存系统的进程和运作流程,并对虚拟机系统优化和异常处理的知识进行了详细讲解。遵循由浅入深的写作方法,按照运作流程逐步分析了Dalvik VM系统的方方面面。本书几乎涵盖了Dalvik VM系统的所有主要内容,并且全书内容言简意赅,讲解方法通俗易懂、详细,不但适合应用高手们的学习,也特别有利于初学者学习并消化。

图书摘要

版权信息

书名:深入解析Android 虚拟机

ISBN:978-7-115-42353-5

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

• 编  著 钟世礼

  责任编辑 张 涛

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Android系统从诞生到现在的短短几年时间里,凭借其易用性和开发的简洁性,赢得了广大开发者的支持。在整个Android系统中,Dalvik VM一直是贯穿从底层内核到高层应用开发的核心。本书循序渐进地讲解了Android虚拟机系统的基本知识,并剖析了其整个内存系统的进程和运作流程,并对虚拟机系统优化和异常处理的知识进行了详细讲解。本书几乎涵盖了Dalvik VM系统的所有主要内容,并且讲解方法通俗易懂,特别有利于读者学习并消化。

本书适合Android初学者、Android底层开发人员、源代码分析人员和虚拟机开发人员学习,也可以作为大专院校相关专业师生的学习用书和培训学校的教材。


Android虚拟机技术——Dalvik VM是通往Android高级开发的必备技术!为了让广大读者深入理解Android系统,不再停留在抽象的原理和概念之上,本书对Android虚拟机方面的知识进行了细致分析,这样做的目的是“提炼”出Android系统的本质,了解Android系统究竟是如何运作的,进程和线程之间是如何协调并进的,内存之间是如何分配并存的。并以此为基础,详细讲解了内存优化、垃圾收集和系统优化方面的基本原理和具体实现。

本书共24章,循序渐进地讲解了Android虚拟机系统的基本知识,从获取并编译Android源码开始,依次讲解了Java虚拟机基础、Android虚拟机基础、分析JNI、分析内存系统、Android程序的生命周期管理、IPC进程通信机制、init进程、Dalvik VM的进程系统、Dalvik VM运作流程、DEX文件、Dvlik VM内存系统、Dalvik VM垃圾收集机制、Dalvik VM内存优化机制、Dalvik VM的启动过程、注册Dalvik VM并创建线程、Dalvik VM异常处理、JIT编译、Dalvik VM内存优化、Dalvik VM性能优化等内容。

在内容的编写上,本书具有以下特色。

(1)结构合理

从用户的实际需要出发,科学安排知识结构,详细讲解了Android虚拟机的各方面知识,内容循序渐进、由浅入深。

(2)遵循“基础讲解—源码分析—核心技术剖析”这一主线

为了使广大读者彻底弄清楚Android虚拟机中的各个知识点,剖析了与Android虚拟机相关的进程运行机制、内存系统、生命周期管理等核心知识,并讲解了读者关心的系统优化技术。

(3)易学易懂

本书内容条理清晰、语言简洁,可以帮助读者快速掌握每个知识点。使读者既可以按照本书编排的章节顺序进行学习,也可以根据自己的需求对某一章节进行有针对性地学习。

由于Android虚拟机系统十分深奥,加上市面上的相关资料十分稀缺。作者在写作过程中对每一段文字都进行了深入研究和推敲,并参阅了国内外大师们的经典资料,对这些资料进行了深入地研读。在作者的写作过程中,从下面4部分资料中获得了帮助。

(1)Oracle官方资料

http://docs.oracle.com/javase/7/docs/

http://docs.oracle.com/javase/6/docs/

http://www.oracle.com/technetwork/java/

上述资料是Oracle官方提供的Java虚拟机资料,这些资料也是国内外读者学习Java虚拟机的第一手资料。

(2)国外经典名著

《The Java Language Specification, Third Edition》

《The Java Virtual Machine Specification》

上述资料是国外大师们根据Oracle官方资料而著成的经典名著,也是国内外读者学习Java虚拟机的参考资料。在国内的一些开源论坛中,有很多热心网友进行了翻译。

(3)Google官方资料

Google I/O 2010 - A JIT Compiler for Android's Dalvik VM

Dalvik VM Internals - Presentation from Google I/O 2008, by Dan Bornstein

Detailed Dalvik specifications documents

上述资料是Google公司《Google I/O讲座系列》的内容,讲解了Android虚拟机优化和内存系统的知识,对广大初学者来说有很强的借鉴作用。当然,Google提供的Android源码更是人们分析Dalvik VM的第一手资料。

(4)国内著作

《解析Java虚拟机开发:权衡优化、高效和安全的最优方案》清华大学出版社,张善香,2013-06-01。

这是国内技术高人的一本著作,可以说是讲解Java虚拟机方面较全的一本参考书。里面介绍的很多内容对写作本书有很大启发,想了解这方面内容的读者可以参考一下。

本书在编写过程中,我的家人在我写作时给予了巨大支持,在此表示深深的感谢。另外,由于本人水平有限,书中如有纰漏和不尽如人意之处在所难免,诚请读者提出意见或建议,以便今后修订并使之更臻完善。另外为本书提供了售后支持网站:http://www.toppr.net/,读者如有疑问可以在此提出,一定会得到满意的答复。编辑联系邮箱:zhangtao@ptpress.com.cn。

作 者


在本章中,将详细讲解获取并编译Android源码的基本知识,介绍各个目录中主要文件的功能,为读者步入本书后面知识的学习打下基础。

要想研究Android系统的源码,需要先获取其源码。目前市面上主流的操作系统有Windows、Linux、Mac OS的操作系统,由于Mac OS源自于Linux系统,因此本书将讲解分别在Windows系统和Linux系统中获取Android源码的知识。

在Linux系统中,通常使用Ubuntu来下载和编译Android源码。由于Android的源码内容很多,Google采用了Git的版本控制工具,并对不同的模块设置不同的Git服务器,可以用repo自动化脚本来下载Android源码,下面介绍获取Android源码的过程。

(1)下载repo。

在用户目录下创建存放repo的bin文件夹,并把该路径设置到环境变量中去,具体命令如下所示:

$ mkdir ~/bin
$ PATH=~/bin:$PATH

下载用于执行repo的repo的脚本,具体命令如下所示:

$ curl https://dl-ssl.google.com/dl/googlesource/git-repo/repo
 > ~/bin/repo

设置可执行权限,命令如下所示:

$ chmod a+x ~/bin/repo

(2)初始化一个repo的客户端。

在用户目录下创建一个空目录,用于存放Android源码,命令如下所示:

$ mkdir AndroidCode
$ cd AndroidCode

进入到AndroidCode目录,并运行repo下载源码,下载主线分支的代码,主线分支包括最新修改的bug,以及并未正式发布版本的最新源码,命令如下所示:

$ repo init -u https://android.googlesource.com/platform/manifest

下载其他分支,建议下载正式发布的版本,可以通过添加-b参数来下载,例如下载Android 4.3正式版的命令如下所示:

$ repo init -u https://android.googlesource.com/platform/manifest -b
android-4.3_r1

在下载过程中会需要填写Name和E-mail,填写完毕之后,选择Y进行确认。最后提示repo初始化完成,这时可以开始同步Android源码了。同步过程非常漫长,需要大家耐心等待。执行下面命令开始同步代码:

$ repo sync

经过上述步骤后,便开始下载并同步Android源码了,界面效果如图1-1所示。

图1-1 下载同步界面

在Windows平台获取源码与在Linux上原理相同,但是需要预先在Windows平台上搭建一个Linux环境,此处需要用到Cygwin工具。Cygwin的作用是构建一套在Windows上的Linux模拟环境,下载Cygwin工具的地址如下所示:

http://cygwin.com/install.html

下载成功后会得到一个名为“setup.exe”的可执行文件,通过此文件可以更新和下载最新的工具版本,具体流程如下所示。

(1)启动Cygwin,如图1-2所示。

图1-2 启动Cygwin

(2)单击“下一步”按钮,选择第一个选项:从网络下载安装,如图1-3所示。

图1-3 选择从网络下载安装

(3)单击“下一步”按钮,选择安装根目录,如图1-4所示。

图1-4 选择安装根目录

(4)单击“下一步”按钮,选择临时文件目录,如图1-5所示。

图1-5 选择临时文件目录

(5)单击“下一步”按钮,设置网络代理。如果所在网络需要代理,则在这一步进行设置,如果不用代理,则选择直接下载,如图1-6所示。

图1-6 设置网络代理

(6)单击“下一步”按钮,选择下载站点。一般选择离得比较近的站点,速度会比较快,如图1-7所示。

图1-7 选择下载站点

(7)单击“下一步”按钮,开始更新工具列表,如图1-8所示。

图1-8 更新工具列表

(8)单击“下一步”按钮,选择需要下载的工具包。在此需要依次下载curl、git、python这些工具,如图1-9所示。

图1-9 依次下载工具

为了确保能够安装上述工具,一定要用鼠标双击这些图标使之变为Install形式,如图1-10所示。

图1-10 务必设置为Install形式  

(9)单击“下一步”按钮,需要经过漫长的等待过程,如图1-11所示。

             

图1-11 下载进度条

如果下载安装成功会出现提示信息,单击“完成”按钮即完成安装。打开安装好的Cygwin后,会模拟出一个Linux的工作环境,然后按照Linux平台的源码下载方法就可以下载Android源码了。

建议读者在下载Android源码时,严格按照官方提供的步骤进行,地址是:http://source. android.com/source/downloading.html,这一点对初学者来说尤为重要。另外,整个下载过程比较漫长,需要大家耐心等待。图1-12是笔者机器的命令截图。

图1-12 在Windows中用Cygwin工具下载Android源码的截图

在作者撰写本书时,Android系统的最新版本是Android L,此版本在Google官方网站的代号为“l-preview”。在Windows系统中获取Android L源码的具体流程如下。

(1)下载Git工具,其官方下载地址是http://www.git-scm.com/downloads ,如图1-13所示。

图1-13 下载Git工具

下载后双击可执行文件进行安装,在安装过程按照默认选项安装即可。

(2)下载并安装TortoiseGit工具,下载地址是https://code.google.com/p/tortoisegit/ ,如图1-14所示。

图1-14 下载TortoiseGit工具

下载后双击下载后的可执行文件进行安装,安装过程按照默认选项安装即可。如果读者对英文不敢兴趣,可以下载TortoiseGit的中文版本,笔者安装的就是TortoiseGit中文版。

(3)新建一个保存源码的文件夹,例如“cc”,在文件夹上单击右键,然后选择“Git克隆”命令。

(4)在弹出的“Git克隆”对话框界面中,在“URL”后面的文本框中输入Android L项目下载路径:https://android.googlesource.com/platform/ manifest.git,如图1-15所示。

图1-15 输入Android L项目下载路径

(5)单击图1-15中的“确定”按钮开始下载分支信息文件,下载后的文件被保存在“cc”文件夹中,具体目录结构如图1-16所示。

图1-16 分支信息文件

(6)打开文件default.xml,在里面列出了Android L源码的各个文件夹中子文件夹目录的分支信息,具体格式如下所示:

<project path="abi/cpp" name="platform/abi/cpp" groups="pdk"/><project path="art" name="platform/ art"/>
<project path="bionic" name="platform/bionic" groups="pdk"/>
<project path="bootable/bootloader/legacy" name="platform/bootable/bootloader/legacy"/>
<project path="bootable/diskinstaller" name="platform/bootable/diskinstaller"/>
<project path="bootable/recovery" name="platform/bootable/recovery" groups="pdk"/>
<project path="cts" name="platform/cts" groups="cts"/>
<project path="dalvik" name="platform/dalvik"/>
<project path="developers/build" name="platform/developers/build"/>
<project path="developers/demos" name="platform/developers/demos"/>
<project path="developers/docs" name="platform/developers/docs"/>
<project path="developers/samples/android" name="platform/developers/samples/android"/>

例如“<project path="bootable/bootloader/legacy" name="platform/bootable/bootloader/legacy"/>”表示在Android L的源码中,存在了一个名为“bootable”的根目录文件夹,而在“bootable”文件夹中又包含了一个名为bootloader”的子文件夹,而在“bootloader”文件夹下又包含了一个名为“legacy”的文件夹。

(7)开始下载Android L源码,在文件夹“cc”上单击右键,然后选择“Git克隆”命令。在弹出界面中输入Android L某个分支的下载路径,例如path=“art”表示“art”,此文件夹的下载地址是:https://android.googlesource.com/ a/art.git。然后勾选“分支”复选框,并在后面填写“l-prevew”分支,如图1-17所示。

图1-17 开始下载“art”文件夹的内容

单击“确定”按钮后将开始下载Android L源码中的“art”文件夹的内容。同理,可以根据default.xml文件提供的路径信息继续下载其他文件夹的内容。

获得Android源码后,可以将整个源码分为如下3个部分。

无论是Android 1.5还是Android 4.3和Android L,各个版本的源码目录基本类似。在里面包含了原始Android的目标机代码、主机编译工具和仿真环境。解压缩下载的Android 4.3源码包后,第一级别目录结构的具体说明如表1-1所示。

表1-1 Android 源码的根目录

Android源码根目录

描  述

abi

abi相关代码,abi:application binary interface,应用程序二进制接口,在Android L中,此文件夹被修改为“api”

art

ART运行环境文件夹,ART机制是从Android 4.4开始推出的运行模式。和传统的Dalvik模式相比,ART模式的运行速度更快,所需的内存更小。从Android L开始,ART被设置为系统默认的运行环境

bionic

bionic C库

bootable

启动引导相关代码

build

存放系统编译规则及generic等基础开发配置包

cts

Android兼容性测试套件标准

dalvik

Dalvik Java虚拟机

development

应用程序开发相关

device

设备相关代码

docs

介绍开源的相关文档

external

android使用的一些开源的模组

frameworks

核心框架——Java及C++语言,是Android应用程序的框架

gdk

即时通信模块

hardware

主要是硬件适配层HAL代码

kernel

Linux的内核文件

libcore

核心库相关

libnativehelper

是Support functions for Android's class libraries的缩写,表示动态库,是实现实现的JNI库的基础。

ndk

ndk相关代码。Android NDK(Android Native Development Kit)是一系列的开发工具,允许程序开发人员在Android应用程序中嵌入C/C++语言编写的非托管代码

out

编译完成后的代码输出在此目录

packages

应用程序包

pdk

Plug Development Kit的缩写,是本地开发套件

prebuilts

x86和ARM架构下预编译的一些资源

sdk

sdk及模拟器

system

文件系统和应用及组件,是用C语言实现的

tools

工具文件夹

vendor

厂商定制代码

Makefile

全局的Makefile

编译Android源码的方法非常简单,只需使用Android源码根目录下的Makefile,执行make命令即可轻松实现。因为Android L是一个Preview版本,官方并没有公布其完整的内核代码。所以本节中的编译内容将以正式版Android 4.3进行。当然在编译Android源码之前,首先要确定已经完成同步工作。进入Android源码目录使用make命令进行编译,使用此命令的格式如下所示:

$: cd ~/Android4.3(这里的“Android4.3”就是我们下载源码的保存目录)
$: make

编译Android源码可以得到“~/project/android/cupcake/out”目录,笔者的截图界面如图1-18所示。

图1-18 编译过程的界面截图

整个编译过程也是非常漫长的,需要读者耐心等待。在本节的内容中,将详细讲解编译Android源码的基本过程。

在编译Android源码之前,需要先进行环境搭建工作。在接下来的内容中,以Ubuntu系统为例讲解搭建编译环境以及编译Android源码的方法。具体流程如下。

(1)安装JDK,编译Android 4.3的源码需要JDK1.6,下载jdk-6u21-linux-i586.bin后进行安装,对应命令如下所示:

$ cd /usr
$ mkdir java
$ cd java
$ sudo cp jdk-6u21-linux-i586.bin所在目录 ./
$ sudo chmod 755 jdk-6u21-linux-i586.bin
$ sudo sh jdk-6u21-linux-i586.bin

(2)设置JDK环境变量,将如下环境变量添加到主文件夹目录下的.bashrc文件中,然后用source命令使其生效,加入的环境变量代码如下所示:

export JAVA_HOME=/usr/java/jdk1.6.0_23
export JRE_HOME=$JAVA_HOME/jre 
export CLASSPATH=.:$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH
export PATH=$PATH:$JAVA_HOME/bin:$JAVA_HOME/bin/tools.jar:$JRE_HOME/bin
export ANDROID_JAVA_HOME=$JAVA_HOME

(3)安装需要的包,读者可以根据编译过程中的提示进行选择,可能需要的包的安装命令如下所示:

$ sudo apt-get install git-core bison zlib1g-dev flex libx11-dev gperf sudo aptitude install git-core gnupg flex bison gperf libsdl-dev libesd0-dev libwxgtk2.6-dev build-essential zip curl libncurses5-dev zlib1g-dev

当完成安装所依赖包的工作后,就可以开始编译Android源码了,具体步骤如下。

(1)首先进行编译初始化工作,在终端中执行以下命令:

source build/envsetup.sh

或:

.build/envsetup.sh

执行后将会输出以下内容。

source build/envsetup.sh 
including device/asus/grouper/vendorsetup.sh
including device/asus/tilapia/vendorsetup.sh
including device/generic/armv7-a-neon/vendorsetup.sh
including device/generic/armv7-a/vendorsetup.sh
including device/generic/mips/vendorsetup.sh
including device/generic/x86/vendorsetup.sh
including device/samsung/maguro/vendorsetup.sh
including device/samsung/manta/vendorsetup.sh
including device/samsung/toroplus/vendorsetup.sh
including device/samsung/toro/vendorsetup.sh
including device/ti/panda/vendorsetup.sh
including sdk/bash_completion/adb.bash

(2)然后选择编译目标,具体命令如下:

lunch full-eng

执行后会输出如下所示的提示信息:

============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=4.3
TARGET_PRODUCT=full
TARGET_BUILD_VARIANT=eng
TARGET_BUILD_TYPE=release
TARGET_BUILD_APPS=
TARGET_ARCH=arm
TARGET_ARCH_VARIANT=armv7-a
HOST_ARCH=x86
HOST_OS=linux
HOST_OS_EXTRA=Linux-3.2.2-5-generic-x86_61-with-Ubuntu-10.01-lucid
HOST_BUILD_TYPE=release
BUILD_ID=JOP40C
OUT_DIR=out
============================================

(3)接下来开始编译代码,在终端中执行以下命令:

make -j4

其中“-j4”表示用4个线程进行编译。整个编译进度根据不同机器的配置而需要不同的时间。例如笔者电脑为Intel i5-2300四核2.8 Hz,4 GB内存,经过近4小时才编译完成。当出现下面的信息时表示编译完成:

target Java: ContactsTests (out/target/common/obj/APPS/ContactsTests_intermediates/classes)
target Dex: Contacts
Done!
Install: out/target/product/generic/system/app/Browser.odex
Install: out/target/product/generic/system/app/Browser.apk
Note: Some input files use or override a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
Copying: out/target/common/obj/APPS/Contacts_intermediates/noproguard.classes.dex
target Package: Contacts (out/target/product/generic/obj/APPS/Contacts_intermediates/package.apk)
 'out/target/common/obj/APPS/Contacts_intermediates/classes.dex' as 'classes.dex'...
Processing target/product/generic/obj/APPS/Contacts_intermediates/package.apk
Done!
Install: out/target/product/generic/system/app/Contacts.odex
Install: out/target/product/generic/system/app/Contacts.apk
build/tools/generate-notice-files.py  out/target/product/generic/obj/NOTICE.txt  out/target/product/generic/obj/NOTICE.html "Notices for files contained in the filesystem images in this directory:" out/target/product/generic/obj/NOTICE_FILES/src
Combining NOTICE files into HTML
Combining NOTICE files into text
Installed file list: out/target/product/generic/installed-files.txt
Target system fs image: out/target/product/generic/obj/PACKAGING/systemimage_intermediates/system.img
Running:  mkyaffs2image -f out/target/product/generic/system out/target/product/generic/obj/PACKAGING/ systemimage_intermediates/system.img
Install system fs image: out/target/product/generic/system.img
DroidDoc took 5331 sec. to write docs to out/target/common/docs/doc-comment-check

在模拟器中运行的步骤就比较简单了,只需在终端中执行下面的命令即可:

emulator

运行成功后的效果如图1-19所示。

图1-19 在模拟器中的编译执行效果

虽然编译方法非常简单,但是作为初学者来说非常容易出错,在下面列出了其中常见的编译错误类型。

(1)缺少必要的软件。

进入到Android目录下,使用make命令进行编译,可能会发现出现如下所示的错误提示。

host C: libneo_cgi <= external/clearsilver/cgi/cgi.c 
external/clearsilver/cgi/cgi.c:22:18: error: zlib.h: No such file or directory

上述错误是因为缺少zlib1g-dev,需要使用apt-get命令从软件仓库中安装zlib1g-dev,具体命令如下所示:

sudo apt-get install zlib1g-dev

同理需要安装下面的软件,否则也会出现上述类似的错误:

sudo apt-get install flex
sudo apt-get install bison
sudo apt-get install gperf
sudo apt-get install libsdl-dev
sudo apt-get install libesd0-dev
sudo apt-get install libncurses5-dev
sudo apt-get install libx11-dev

(2)没有安装Java环境JDK。

当安装所有上述软件后,运行make命令再次编译Android源码。如果在之前忘记安装Java环境JDK,则此时会出现很多Java文件无法编译的错误,如果打开Android的源码,可以在如下目录中下发现有很多Java源文件。

android/dalvik/libcore/dom/src/test/java/org/w3c/domts

这充分说明在编译Android之前必须先安装Java环境JDK,安装流程如下所示。

① 登录Oracle官方网站,下载jdk-6u16-linux-i586.bin文件并安装。

在Ubuntu 8.04中,“/etc/profile”文件是全局的环境变量配置文件,它适用于所有的shell。在登录Linux系统时应该先启动“/etc/profile”文件,然后再启动用户目录下的“~/.bash_profile”、“~/.bash_login”或“~/.profile”文件中的其中一个,执行的顺序和上面的排序一样。如果“~/.bash_profile”文件存在,则还会执行“~/.bashrc”文件。在此只需要把JDK的目录放到“/etc/profile”目录下即可:

JAVA_HOME=/usr/local/src/jdk1.6.0_16
PATH=$PATH:$JAVA_HOME/bin:/usr/local/src/android-sdk-linux_x86-1.1_r1/tools:~/bin

② 重新启动计算机,输入java –version命令,输出下面的信息则表示配置成功:

ava version "1.6.0_16"
Java(TM) SE Runtime Environment (build 1.6.0_16-b01)
Java HotSpot(TM) Client VM (build 13.1-b01, mixed mode, sharing)

当成功编译Android源码后,在终端会输出如下提示:

Target system fs image: out/target/product/generic/obj/PACKAGING/systemimage_unopt_intermediates/system.img
Install system fs image: out/target/product/generic/system.img
Target ram disk: out/target/product/generic/ramdisk.img
Target userdata fs image: out/target/product/generic/userdata.img
Installed file list: out/target/product/generic/installed-files.txt
root@dfsun2009-desktop:/bin/android#

Android编译环境本身比较复杂,并且不像普通的编译环境那样只有顶层目录下才有Makefile文件,而其他的每个Component都使用统一标准的Android.mk文件。不过这并不是我们熟悉的Makefile,而是经过Android自身编译系统的很多处理。所以说要真正理清楚其中的联系还比较复杂,不过这种方式的好处在于,编写一个新的Android.mk给Android增加一个新的Component会变得比较简单。为了使读者更加深入地理解在Linux环境下编译Android程序的方法,在接下来的内容中,将分别演示两种编译Android程序的方法。

1.编译Native C(本地C程序)的helloworld模块

编译Java程序可以直接采用Eclipse的集成环境来完成,实现方法非常简单,在这里就不再重复了。接下来将主要针对C/C++进行说明,通过一个例子来讲解在Android 中增加一个C程序的Hello World的方法。

(1)在“$(YOUR_ANDROID)/development”目录下创建一个名为“hello”的目录,并用“$(YOUR_ANDROID)”指向Android源代码所在的目录:

- # mkdir $(YOUR_ANDROID)/development/hello

(2)在目录“$(YOUR_ANDROID)/development/hello/”下编写一个名为“hello.c”的C语言文件,文件hello.c的实现代码如下所示:

#include <stdio.h>
int main()
{
    printf("Hello World!\n");//输出Hello World
return 0;
}

(3)在目录“$(YOUR_ANDROID)/development/hello/”下编写Android.mk文件。这是Android Makefile的标准命名,不能更改。文件Android.mk的格式和内容可以参考其他已有的Android.mk文件的写法,针对helloworld程序的Android.mk文件内容如下所示:

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_SRC_FILES:= \
    hello.c
LOCAL_MODULE := helloworld
include $(BUILD_EXECUTABLE)

上述各个内容的具体说明如下所示。

(4)回到Android源代码顶层目录进行编译。

# cd $(YOUR_ANDROID) && make helloworld

在此需要注意,make helloworld中的目标名helloworld就是上面Android.mk文件中由LOCAL_MODULE指定的模块名。最终的编译结果如下所示:

target thumb C: helloworld <= development/hello/hello.c
target Executable: helloworld (out/target/product/generic/obj/EXECUTABLES/helloworld_intermediates/ LINKED/helloworld)
target Non-prelinked: helloworld (out/target/product/generic/symbols/system/bin/helloworld)
target Strip: helloworld (out/target/product/generic/obj/EXECUTABLES/helloworld_intermediates/ helloworld)
Install: out/target/product/generic/system/bin/helloworld

(5)如果和上述编译结果相同,则编译后的可执行文件存放在如下目录:

out/target/product/generic/system/bin/helloworld

这样通过“adb push”将它传送到模拟器上,再通过“adb shell”登录到模拟器终端后就可以执行了。

2.手工编译C模块

在前面讲解了通过标准的Android.mk文件来编译C模块的具体流程,其实可以直接运用gcc命令行来编译C程序,这样可以更好地了解Android编译环境的细节。具体流程如下。

(1)在Android编译环境中,提供了“showcommands”选项来显示编译命令行,可以通过打开这个选项来查看一些编译时的细节。

(2)在具体操作之前需要使用如下命令把前面中的helloworld模块清除:

# make clean-helloworld

上面的“make clean-$(LOCAL_MODULE)”命令是Android编译环境提供的make clean的方式。

(3)使用showcommands选项重新编译helloworld,具体命令如下所示:

# make helloworld showcommands
build/core/product_config.mk:229: WARNING: adding test OTA key
target thumb C: helloworld <= development/hello/hello.c
prebuilt/linux-x86/toolchain/arm-eabi-4.3.1/bin/arm-eabi-gcc  -I system/core/include   -I hardware/libhardware/include   -I hardware/ril/include   -I dalvik/libnativehelper/include   -I frameworks/base/include   -I external/skia/include   -I out/target/product/generic/obj/include   -I bionic/libc/arch-arm/include   -I bionic/libc/include   -I bionic/libstdc++/include   -I bionic/libc/kernel/common   -I bionic/libc/kernel/arch-arm   -I bionic/libm/include   -I bionic/libm/include/arch/arm   -I bionic/libthread_db/include   -I development/hello   -I out/target/product/generic/obj/EXECUTABLES/helloworld_intermediates  -c  -fno-exceptions -Wno-multichar -march=armv5te -mtune=xscale -msoft-float -fpic -mthumb-interwork -ffunction-sections -funwind-tables -fstack-protector -D__ARM_ARCH_5__ -D__ARM_ARCH_5T__ -D__ARM_ARCH_5E__ -D__ARM_ARCH_5TE__ -include system/core/include/arch/linux-arm/AndroidConfig.h -DANDROID -fmessage-length=0 -W -Wall -Wno-unused -DSK_RELEASE -DNDEBUG -O2 -g -Wstrict-aliasing=2 -finline-functions -fno-inline-functions-called-once -fgcse-after-reload -frerun-cse-after-loop -frename-registers -DNDEBUG -UDEBUG -mthumb -Os -fomit-frame-pointer -fno-strict-aliasing -finline-limit=64      -MD -o out/target/product/generic/ obj/EXECUTABLES/helloworld_intermediates/hello.o development/hello/hello.c

target Executable: helloworld (out/target/product/generic/obj/EXECUTABLES/helloworld_intermediates/ LINKED/helloworld)

prebuilt/linux-x86/toolchain/arm-eabi-4.3.1/bin/arm-eabi-g++ -nostdlib -Bdynamic -Wl,-T,build/core/armelf.x -Wl,-dynamic-linker,/system/bin/linker -Wl,--gc-sections -Wl,-z,nocopyreloc -o out/target/product/generic/ obj/EXECUTABLES/helloworld_intermediates/LINKED/helloworld -Lout/target/product/generic/obj/lib -Wl, -rpath-link=out/target/product/generic/obj/lib -lc -lstdc++ -lm  out/target/product/generic/obj/lib/ crtbegin_dynamic.o         out/target/product/generic/obj/EXECUTABLES/helloworld_intermediates/hello.o       -Wl,--no-undefined prebuilt/linux-x86/toolchain/arm-eabi-4.3.1/bin/../lib/gcc/arm-eabi/4.3.1/interwork/libgcc.a out/target/product/generic/obj/lib/crtend_android.o

target Non-prelinked: helloworld (out/target/product/generic/symbols/system/bin/helloworld)

out/host/linux-x86/bin/acp -fpt out/target/product/generic/obj/EXECUTABLES/helloworld_intermediates/ LINKED/helloworld out/target/product/generic/symbols/system/bin/helloworld

target Strip: helloworld (out/target/product/generic/obj/EXECUTABLES/helloworld_intermediates/helloworld)

out/host/linux-x86/bin/soslim --strip --shady --quiet out/target/product/generic/symbols/system/bin/ helloworld --outfile out/target/product/generic/obj/EXECUTABLES/helloworld_intermediates/helloworld

Install: out/target/product/generic/system/bin/helloworld

out/host/linux-x86/bin/acp -fpt out/target/product/generic/obj/EXECUTABLES/helloworld_intermediates/ helloworld out/target/product/generic/system/bin/helloworld

从上述命令行可以看到,Android编译环境所用的交叉编译工具链如下所示:

prebuilt/linux-x86/toolchain/arm-eabi-4.3.1/bin/arm-eabi-gcc

其中参数“-I”和“-L”分别指定了所用的C库头文件和动态库文件路径分别是“bionic/libc/include ”和“out/target/product/generic/obj/lib”,其他还包括很多编译选项以及-D所定义的预编译宏。

(4)此时就可以利用上面的编译命令来手工编译helloworld程序,首先手工删除上次编译得到的helloworld程序:

# rm out/target/product/generic/obj/EXECUTABLES/helloworld_intermediates/hello.o
# rm out/target/product/generic/system/bin/helloworld

然后再用gcc编译以生成目标文件:

# prebuilt/linux-x86/toolchain/arm-eabi-4.3.1/bin/arm-eabi-gcc -I bionic/libc/arch-arm/include -I bionic/libc/include -I bionic/libc/kernel/common   -I bionic/libc/kernel/arch-arm -c  -fno-exceptions -Wno-multichar -march=armv5te -mtune=xscale -msoft-float -fpic -mthumb-interwork -ffunction-sections -funwind-tables -fstack-protector -D__ARM_ARCH_5__ -D__ARM_ARCH_5T__ -D__ARM_ARCH_5E__ -D__ARM_ARCH_5TE__ -include system/core/include/arch/linux-arm/AndroidConfig.h -DANDROID -fmessage-length=0 -W -Wall -Wno-unused -DSK_RELEASE -DNDEBUG -O2 -g -Wstrict-aliasing=2 -finline-functions -fno-inline-functions-called-once -fgcse-after-reload -frerun-cse-after-loop -frename-registers -DNDEBUG -UDEBUG -mthumb -Os -fomit-frame-pointer -fno-strict-aliasing -finline-limit=64      -MD -o out/target/product/generic/obj/EXECUTABLES/helloworld_intermediates/hello.o development/hello/hello.c

如果此时与Android.mk编译参数进行比较,会发现上面主要减少了不必要的参数“-I”。

(5)接下来开始生成可执行文件:

# prebuilt/linux-x86/toolchain/arm-eabi-4.3.1/bin/arm-eabi-gcc -nostdlib -Bdynamic -Wl,-T,build/core/ armelf.x -Wl,-dynamic-linker,/system/bin/linker -Wl,--gc-sections -Wl,-z,nocopyreloc -o out/target/ product/generic/obj/EXECUTABLES/helloworld_intermediates/LINKED/helloworld -Lout/target/product/generic/obj/lib -Wl,-rpath-link=out/target/product/generic/obj/lib -lc -lm  out/target/product/generic/obj/EXECUTABLES/ helloworld_intermediates/hello.o out/target/product/generic/obj/lib/crtbegin_dynamic.o -Wl,--no- undefined ./prebuilt/linux-x86/toolchain/arm-eabi-4.3.1/bin/../lib/gcc/arm-eabi/4.3.1/interwork/libgcc.a out/target/product/generic/obj/lib/crtend_android.o

在此需要特别注意的是参数“-Wl,-dynamic-linker,/system/bin/linker”,它指定了Android专用的动态链接器是“/system/bin/linker”,而不是平常使用的ld.so。

(6)最后可以使用命令file和readelf来查看生成的可执行程序:

# file out/target/product/generic/obj/EXECUTABLES/helloworld_intermediates/LINKED/helloworld
out/target/product/generic/obj/EXECUTABLES/helloworld_intermediates/LINKED/helloworld: ELF 31-bit LSB executable, ARM, version 1 (SYSV), dynamically linked (uses shared libs), not stripped
#  readelf -d out/target/product/generic/obj/EXECUTABLES/helloworld_intermediates/LINKED/helloworld |grep NEEDED
 0x00000001 (NEEDED)                     Shared library: [libc.so]
 0x00000001 (NEEDED)                     Shared library: [libm.so]

这就是ARM格式的动态链接可执行文件,在运行时需要libc.so和libm.so。当提示“not stripped”时表示它还没被STRIP(剥离)。嵌入式系统中为节省空间通常将编译完成的可执行文件或动态库进行剥离,即去掉其中多余的符号表信息。在前面“make helloworld showcommands”命令的最后也可以看到,Android编译环境中使用了“out/host/linux-x86/bin/soslim”工具进行STRIP。

编译Android Kernel代码就是编译Android内核代码,在进行具体编译工作之前,需要先了解在Android开源系统中包含的以下3部分代码。

在本节的内容中,将详细讲解编译上述Android Kernel的基本知识。

Goldfish是一种虚拟的ARM处理器,通常在Android的仿真环境中使用。在Linux的内核中,Goldfish作为ARM体系结构的一种“机器”。在Android的发展过程中,Goldfish内核的版本也从Linux 2.6.25升级到了Linux 3.4,此处理器的Linux内核和标准的Linux内核有以下3个方面的差别。

Goldfish处理器有两个版本,分别是ARMv5和ARMv7,在一般情况下,只需使用ARMv5版本即可。在Android开源工程的代码仓库中,使用git工具得到Goldfish内核代码的命令如下所示:

$ git clone git://android.git.kernel.org/kernel/common.git

在其Linux源代码的根目录中,配置和编译Goldfish内核的过程如下所示:

$make ARCH=arm goldfish_defconfig .config
$make ARCH=arm CROSS_COMPILE={path}/arm-none-linux-gnueabi-

其中,CROSS_COMPILE的path值用于指定交叉编译工具的路径。

编译结果如下所示:

LD vmlinux
SYSMAP system.map
SYSMAP .tmp_system.map
OBJCOPY arch/arm/boot/Image
Kernel: arch/arm/boot/Image is ready
AS arch/arm/boot/compressed/head.o
GZIP arch/arm/boot/compressed/piggy.gz
AS arch/arm/boot/compressed/piggy.o
CC arch/arm/boot/compressed/misc.o
LD arch/arm/boot/compressed/vmlinux
   OBJCONPY arch/arm/boot/zImage
   Kernel: arch/arm/boot/zImage is ready

在Android源代码的根目录中,vmlinux和zImage分别对应Android代码prebuilt中的预编译的ARM内核。使用zImage可以替换 prebuilt中的“prebuilt/android-arm/”目录下的goldfish_defconfig,此文件的主要片断如下所示:

CONFIG_ARM=y
#
# System Type
#
CONFIG_ARCH_GOLDFISH=y
#
# Goldfish options
#
CONFIG_MACH_GOLDFISH=y
# CONFIG_MACH_GOLDFISH_ARMV7 is not set

因为GoldFish是ARM处理器,所以CONFIG_ARM宏需要被使能,CONFIG_ARCH_GOLDFISH和CONFIG_MACH_GOLDFISH宏是GoldFish处理器这类机器使用的配置宏。

在gildfish_defconfig中,与Android系统相关的宏如下所示:

#
# android
#
CONFIG_ANDROID=y
CONFIG_ANDROID_BUNDER_IPC=y #binder ipc驱动程序
CONFIG_ANDROID_LOGGER=y #log记录器驱动程序
# CONFIG_ANDROID_RAM_CONSOLE is not set
CONFIG_ANDROID_TIMED_OUTPUT=y #定时输出驱动程序框架
CONFIG_ANDROID_LOW_MEMORY_KILLER=y
CONFIG_ANDROID_PMEM=y #物理内存驱动程序
CONFIG_ASHMEM=y #匿名共享内存驱动程序
CONFIG_RTC_INTF_ALARM=y
CONFIG_HAS_WAKELOCK=y 电源管理相关的部分wakelock和earlysuspend
CONFIG_HAS_EARLYSUSPEND=y
CONFIG_WAKELOCK=y
CONFIG_WAKELOCK_STAT=y
CONFIG_USER_WAKELOCK=y
CONFIG_EARLYSUSPEND=y
goldfish_defconfig 配置文件中,另外有一个宏是处理器虚拟设备的“驱动程序”,其内容如下所示:
CONFIG_MTD_GOLDFISH_NAND=y
CONFIG_KEYBOARD_GOLDFISH_EVENTS=y
CONFIG_GOLDFISH_TTY=y
CONFIG_BATTERY_GOLDFISH=y
CONFIG_FB_GOLDFISH=y
CONFIG_MMC_GOLDFISH=y
CONFIG_RTC_DRV_GOLDFISH=y

在Goldfish处理器的各个配置选项中,体系结构和Goldfish的虚拟驱动程序基于标准Linux内容的驱动程序框架,但是这些设备在不同硬件平台的移植方式不同;Android专用的驱动程序是Android中特有的内容,非Linux标准,但是和硬件平台无关。

和原Linux内核相比,Android内核增加了Android的相关驱动(Driver),对应的目录如下所示:

kernel/drivers/android

Android的相关驱动主要分为以下几类驱动。

对于本书讲解的驱动程序开发来说,我们比较关心的是GoldFish平台下相关的驱动文件,具体说明如下所示。

(1)字符输出设备:

kernel/drivers/char/goldfish_tty.c

(2)图象显示设备(Frame Buffer):

kernel/drivers/video/goldfishfb.c

(3)键盘输入设备文件:

kernel/drivers/input/keyboard/goldfish_events.c

(4)RTC设备(Real Time Clock)文件:

kernel/drivers/rtc/rtc-goldfish.c

(5)USB Device设备文件:

kernel/drivers/usb/gadget/android_adb.c

(6)SD卡设备文件:

kernel/drivers/mmc/host/goldfish.c

(7)FLASH设备文件:

kernel/drivers/mtd/devices/goldfish_nand.c
kernel/drivers/mtd/devices/goldfish_nand_reg.h

(8)LED设备文件:

kernel/drivers/leds/ledtrig-sleep.c

(9)电源设备:

kernel/drivers/power/goldfish_battery.c

(10)音频设备:

kernel/arch/arm/mach-goldfish/audio.c

(11)电源管理:

kernel/arch/arm/mach-goldfish/pm.c

(12)时钟管理:

kernel/arch/arm/mach-goldfish/timer.c

在目前市面上,谷歌的手机产品G1是基于MSM内核的,MSM是高通公司的应用处理器,在Android代码库中公开了对应的MSM的源代码。在Android开源工程的代码仓库中,使用Git工具得到MSM内核代码的命令如下所示:

$ git clone git://android.git.kernel.org/kernel/msm.git

OMAP是德州仪器公司的应用处理器,为Android使用的是OMAP3系列的处理器。在Android代码库中公开了对应的OMAP的源代码,使用Git工具得到OMAP内核代码的命令如下所示:

$ git clone git://android.git.kernel.org/kernel/omap.git

了解了上述3类Android内核后,下面开始讲解编译Android内核的方法。在此以Ubuntu 8.10为例,完整编译Android内核的流程如下。

(1)构建交叉编译环境。

Android的默认硬件处理器是ARM,因此需要在自己的机器上构建交叉编译环境。交叉编译器GNU Toolchain for ARM Processors下载地址如下所示:

http://www.codesourcery.com/gnu_toolchains/arm/download.html

单击GNU/Linux对应的链接,再单击“Download Sourcery CodeBench Lite 5.1 2012.03-117”链接后直接下载,如图1-20所示。

图1-20 下载交叉编译器

把arm-2008q3-71-arm-none-linux-gnueabi-i686-pc-linux- gnu.tar.bz2解压到一目录下,例如“~/programes/”,并加入PATH环境变量:

vim ~/.bashrc

然后添加:

ARM_TOOLCHIAN=~/programes/arm-2008q3/bin/
export PATH=${PATH}:${ARM_TOOLCHIAN};

保存后并source ~/.bashrc。

(2)获取内核源码,源码地址如下所示:

http://code.google.com/p/android/downloads/list

选择的内核版本要与选用的模拟器版本尽量一致。下载并解压后得到kernel.git文件夹。

tar -xvf ~/download/linux-3.2.5-android-4.3_r1.tar.gz

(3)获取内核编译配置信息文件。

编译内核时需要使用configure,通常configure有很多选项,我们往往不知道需要那些选项。在运行Android模拟器时,有一个文件“/proc/config.gz”,这是当前内核的配置信息文件,把config.gz获取并解压到“kernel.git/”下,然后改名为.config。命令如下所示:

cd kernel.git/
emulator &
adb pull /proc/config.gz
gunzip config.gz
mv config .config

(4)修改Makefile。

修改195行的代码:

CROSS_COMPILE    = arm-none-linux-gnueabi-

将CROSS_COMPILE值改为arm-none-linux-gnueabi-,这是安装的交叉编译工具链的前缀,修改此处意在告诉make在编译的时候要使用该工具链。然后注释掉562和563行的如下代码:

#LDFLAGS_BUILD_ID = $(patsubst -Wl$(comma)%,%,/
#                  $(call ld-option, -Wl$(comma)--build-id,))

必须将上述代码中的build id 值注释掉,因为目前版本的Android内核不支持该选项。

(5)编译。

使用make进行编译,并同时生成zImage:

  LD      arch/arm/boot/compressed/vmlinux
  OBJCOPY arch/arm/boot/zImage
  Kernel: arch/arm/boot/zImage is ready

这样生成zImage大小为1.23 MB,android- sdk-linux_x86-4.3_r1/tools/lib/images/kernel-qemu是1.24 MB。

(6)使用模拟器加载内核测试,命令如下所示:

cd android/out/cupcake/out/target/product/generic
emulator -image system.img -data userdata.img -ramdisk ramdisk.img -kernel ~/project/android/kernel.git/ arch/arm/boot/zImage &

到此为止,模拟器就加载成功了。


Java虚拟机和Android虚拟机十分相似,所以在本书中将以Java虚拟机开始,逐步引领广大读者步入Android虚拟机的世界。在本章的内容中,将简要讲解Java虚拟机技术的基本知识,为读者步入本书后面知识的学习打下基础。

虚拟机(Virtual Machine)这一概念最初由波佩克与戈德堡定义,是指通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。由此可见,虚拟机是跟特定硬件无关的一个系统。在现实应用中,虚拟机最常见的情形便是双系统。例如计算机原装系统是Windows,为了在这台机器上能够体验Linux系统,可以安装一个虚拟机环境,在这个虚拟机环境中运行Linux系统,这样就实现了“一机双系统”的功效。在现实应用中,通过虚拟机软件可以在一台物理计算机上模拟出一台或多台虚拟的计算机。这些虚拟机完全可以像真正的计算机那样进行工作,例如可以安装操作系统、安装应用程序、访问网络资源等等。对于使用用户而言,虚拟机只是运行在物理计算机上的一个应用程序。但是对于在虚拟机中运行的应用程序来说,虚拟机就是一台真正计算机。正因为如此,所以当在虚拟机中进行软件评测时,可能会发生系统崩溃的情形。但是这里崩溃的只是虚拟机上的操作系统,而不是物理计算机上的操作系统。可以使用虚拟机的“Undo”(恢复)功能,立即恢复虚拟机到安装软件之前的状态。

虚拟机根据它们的运用以及与直接机器的相关性分为两大类。系统虚拟机提供一个可以运行完整操作系统的完整系统平台;相反,程序虚拟机为运行单个计算机程序设计,这意谓它支持单个进程。虚拟机的一个本质特点是运行在虚拟机上的软件被局限在虚拟机提供的资源里——它不能超出虚拟世界。

在现实应用中,对于一般计算机用户来说,最常见的使用虚拟机的情形是安装双系统。例如在Windows平台上安装一个虚拟机,然后在这个虚拟机中安装Linux操作系统或iOS系统,这样就实现了双系统功能。

在当前流行的编程语言Java中,便是采用了虚拟机机制,Java的虚拟机被称为Java Virtual Machine,缩写为JVM。用Java编写的程序可以通过对Java运行环境(JRE)软件发出命令获得服务,取得期望的结果。透过提供这种服务,JRE起到了虚拟机的作用,程序不必为特定的操作系统或硬件编写。

Java虚拟机和Android虚拟机十分相似,所以在本书中将以Java虚拟机开始,逐步引领广大读者步入Android虚拟机的世界。

Java虚拟机(JVM)是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能模拟来实现的。Java虚拟机有自己完善的硬件架构,如处理器、堆栈、寄存器等,还具有相应的指令系统。JVM虚拟机的运作结构如图2-1所示。

从该图中可以看到,JVM是运行在操作系统之上的,与硬件没有直接的交互。JVM的具体组成部分如图2-2所示。

图2-1 JVM虚拟机的运作结构

图2-2 JVM构成图

(1)使用JVM的原因。

Java语言的一个非常重要的特点就是与平台的无关性。而使用JVM是实现这一特点的关键。一般的高级语言如果要在不同的平台上运行,至少需要编译成不同的目标代码。在引入JVM后,Java语言在不同平台上运行时不需要重新编译。Java语言使用模式JVM屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在JVM上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。当JVM执行字节码时,把字节码解释成具体平台上的机器指令执行。

(2)JVM的作用。

JVM是Java语言底层实现的基础,对Java语言感兴趣的读者来说,很有必要对Java虚拟机有一个大概的了解。因为这不但有助于理解Java语言的一些性质,而且也有助于使用Java语言。对于要在特定平台上实现JVM的软件人员、Java语言的编译器作者以及要用硬件芯片实现JVM的人员来说,必须深刻理解JVM的规范。另外,如果你想扩展Java语言,或是把其他语言编译成Java语言的字节码,你也需要深入地了解JVM。

在本节的内容中,将简要讲解和JVM相关的基本知识。

在JVM机制中,可以支持如下所示的基本数据类型。

几乎所有的Java类型检查工作都是在编译时完成的,上述列出的原始数据类型数据在Java执行时不需要用硬件标记。操作这些原始数据类型数据的字节码(指令)本身就已经指出了操作数的数据类型,例如iadd、ladd、fadd和dadd指令都是把两个数相加,其操作数类型分别是int、long、float和double。虚拟机没有给boolean(布尔)类型设置单独的指令。boolean型的数据是由integer指令,包括integer返回来处理的。boolean型的数组则是用byte数组来处理的。虚拟机使用IEEE754格式的浮点数,不支持IEEE格式的较旧的计算机,在运行Java数值计算程序时,可能会非常慢。

虚拟机的规范对于object内部的结构没有任何特殊的要求。在Oracle公司的实现中,对object的引用是一个句柄,其中包含一对指针:一个指针指向该object的方法表,另一个指向该object的数据。用Java虚拟机的字节码表示的程序应该遵守类型规定。Java虚拟机的实现应拒绝执行违反了类型规定的字节码程序。Java虚拟机由于字节码定义的限制似乎只能运行于32位地址空间的机器上。但是可以创建一个Java虚拟机,它自动地把字节码转换成64位的形式。从Java虚拟机支持的数据类型可以看出,Java对数据类型的内部格式进行了严格规定,这样使得各种Java虚拟机的实现对数据的解释是相同的,从而保证了Java的与平台无关性和可移植性。

JVM由如下5个部分组成。

这5部分是Java虚拟机的逻辑成分,不依赖任何实现技术或组织方式,但它们的功能必须在真实机器上以某种方式实现。在接下来的内容中,将简要介绍上述组成部分的基本知识,更加详细的知识读者可以参阅本书后面的内容。

1.Java指令集

Java虚拟机支持大约248个字节码,每个字节码执行一种基本的CPU运算,例如把一个整数加到寄存器,子程序转移等。Java指令集相当于Java程序的汇编语言。

Java指令集中的指令包含一个单字节的操作符,用于指定要执行的操作,还有0个或多个操作数,提供操作所需的参数或数据。许多指令没有操作数,仅由一个单字节的操作符构成。

虚拟机的内层循环的执行过程如下:

do{
取一个操作符字节;
根据操作符的值执行一个动作;
}while(程序未结束)

由于指令系统的简单性,使得虚拟机执行的过程十分简单,这样有利于提高执行的效率。指令中操作数的数量和大小是由操作符决定的。如果操作数比一个字节大,那么它存储的顺序是高位字节优先。假如一个16位的参数存放时占用两个字节,其值为:

第一个字节*256+第二个字节

字节码指令流一般只是字节对齐的,但是指令tabltch和lookup是例外,在这两条指令内部要求强制的4字节边界对齐。

2.寄存器

Java虚拟机的寄存器用于保存机器的运行状态,与微处理器中的某些专用寄存器类似,所有寄存器都是32位的。在Java虚拟机中有以下4种寄存器。

Java虚拟机是栈式的,它不定义或使用寄存器来传递或接收参数,其目的是为了保证指令集的简洁性和实现时的高效性,特别是对于寄存器数目不多的处理器。

3.栈

Java虚拟机中的栈有3个区域,分别是局部变量区、运行环境区、操作数区。

(1)局部变量区。

每个Java方法使用一个固定大小的局部变量集。它们按照与vars寄存器的字偏移量来寻址。局部变量都是32位的。长整数和双精度浮点数占据了两个局部变量的空间,却按照第一个局部变量的索引来寻址(例如,一个具有索引n的局部变量,如果是一个双精度浮点数,那么它实际占据了索引nn+1所代表的存储空间)。虚拟机规范并不要求在局部变量中的64位的值是64位对齐的。虚拟机提供了把局部变量中的值装载到操作数栈的指令,也提供了把操作数栈中的值写入局部变量的指令。

(2)运行环境区。

在运行环境中包含的信息可以实现动态链接、正常的方法返回和异常、错误传播。

运行环境包括对指向当前类和当前方法的解释器符号表的指针,用于支持方法代码的动态链接。方法clas文件代码在引用要调用的方法和要访问的变量时使用符号。动态链接把符号形式的方法调用翻译成实际方法调用,装载必要的类以解释还没有定义的符号,并把变量访问翻译成与这些变量运行时的存储结构相应的偏移地址。动态链接方法和变量使得方法中使用的其他类的变化不会影响到本程序的代码。

如果当前方法正常地结束了,在执行了一条具有正确类型的返回指令时,调用的方法会得到一个返回值。执行环境在正常返回的情况下用于恢复调用者的寄存器,并把调用者的程序计数器增加一个恰当的数值,以跳过已执行过的方法调用指令,然后在调用者的执行环境中继续执行下去。

异常情况在Java中被称作Error(错误)或Exception(异常),是Throwable类的子类,在程序中的原因有如下两点。

(3)操作数栈区。

机器指令只从操作数栈中取操作数,对它们进行操作,并把结果返回到栈中。选择栈结构的原因是:在只有少量寄存器或非通用寄存器的机器(如Intel486)上,也能够高效地模拟虚拟机的行为。操作数栈是32位的。它用于给方法传递参数,并从方法接收结果,也用于支持操作的参数,并保存操作的结果。例如,iadd指令将两个整数相加,相加的两个整数应该是操作数栈顶的两个字,这两个字是由先前的指令压进堆栈的,这两个整数将从堆栈弹出、相加,并把结果压回到操作数栈中。

每个原始数据类型都有专门的指令对它们进行必须的操作。每个操作数在栈中需要一个存储位置,除了long和double型,它们需要两个位置。操作数只能被适用于其类型的操作符所操作。例如压入两个int类型的数,如果把它们当作是一个long类型的数则是非法的。在Sun的虚拟机实现中,这个限制由字节码验证器强制实行。但是有少数操作(操作符dupe和swap),用于对运行时数据区进行操作时是不考虑类型的。

4.无用单元收集堆

Java的堆是一个运行时数据区,类的实例(对象)从中分配空间。Java语言具有无用单元收集能力,即它不给程序员显示释放对象的能力。Java不规定具体使用的无用单元收集算法,可以根据系统的需求使用各种各样的算法。

5.方法区

方法区与传统语言中的编译后代码或是Unix进程中的正文段类似。它保存方法代码(编译后的java代码)和符号表。在当前的Java实现中,方法代码不包括在无用单元收集堆中,但计划在将来的版本中实现。每个类文件包含了一个Java类或一个Java界面的编译后的代码。可以说类文件是Java语言的执行代码文件。为了保证类文件的平台无关性,Java虚拟机规范中对类文件的格式也作了详细的说明。其具体细节请参考Sun公司的Java虚拟机规范。

在Java虚拟机规范中,一个虚拟机实例的行为是分别按照子系统、内存区、数据类型以及指令这几个术语来描述的。这些组成部分一起展示了抽象的虚拟机的内部抽象体系结构。但是规范中对它们的定义并非要强制规定Java虚拟机实现内部的体系结构,更多的是为了严格地定义这些实现的外部特征。规范本身通过定义这些抽象的组成部分以及它们之间的交互,来定义任何Java虚拟机实现都必须遵守的行为。

图2-3是Java虚拟机的结构框图,包括在规范中描述的主要子系统和内存区。前一章曾提到,每个Java虚拟机都有一个类装载器子系统,它根据给定的全限定名类装入类型(类或接口),同样,每个Java虚拟机都有一个执行引擎,它负责执行那些包含在被装载类的方法中的指令。

图2-3 Java虚拟机的内部体系结构

当Java虚拟机运行一个程序时,它需要使用内存来存储许多东西,例如下面所示的元素。

Java虚拟机会把上述元素都组织到几个“运行时数据区”中,目的是便于管理。尽管这些“运行时数据区”都会以某种形式存在于每一个Java虚拟机实现中,但是规范对它们的描述却是相当抽象的。这些运行时数据区结构上的细节,大多数都由具体实现的设计者决定。

不同的虚拟机实现可能具有很不同的内存限制,有的实现可能大量的内存可用,有的可能只有很少的内存,有的实现可以利用虚拟内存,有的则不能。规范本身对“运行时数据区”只有抽象的描述,这就使得Java虚拟机可以很容易地在各种计算机和设备上实现。

某些运行时数据区是由程序汇总所有线程共享的,还有一些则只由一个线程拥有。每个Java虚拟机实例都有一个方法区以及一个堆,它们是由该虚拟机实例中所有线程共享的。当虚拟机装载一个class文件时,它会从这个class文件包含的二进制数据中解析类型信息,然后,它把这些类型信息放到方法区中。当程序运行时,虚拟机会把所有该程序在运行时创建的对象都放到堆中。图2-4对这些内存区域进行了描绘。

图2-4 由所有线程共享的运行时数据区

当每一个新线程被创建时,它都将得到它自己的PC寄存器(程序计数器)以及一个Java栈:如果线程正在执行的是一个Java方法(非本地方法),那么PC寄存器的值将总是指示下一条将被执行的指令,而它的Java栈则总是存储该线程中Java方法调用的状态——包括它的局部变量、被调用时传进来的参数、它的返回值以及运算的中间结果等。而本地方法调用的状态,则是以某种依赖于具体实现的方式存储在本地方法栈中,也可能是在寄存器或者其他某些与特定实现相关的内存区中。

Java栈是由许多栈帧(stackframe)或者说帧(frame)组成的,一个栈帧包含一个Java方法调用的状态。当线程调用一个Java方法时,虚拟机压入一个新的栈帧到该线程的Java栈中。当该方法返回时,这个栈帧被从Java栈中弹出并抛弃。

Java虚拟机没有寄存器,其指令集使用Java栈来存储中间数据。这样设计的原因是为了保持Java虚拟机的指令集尽量紧凑,同时也便于Java虚拟机在那些只有很少通用寄存器的平台上实现,另外Java虚拟机的这种基于栈的体系结构,也有助于运行时某些虚拟机实现的动态编译器和即时编译器的代码优化。

图2-5描绘了Java虚拟机为每一个线程创建的内存区,这些内存区域是私有的,任何线程都不能访问另一个线程的PC寄存器或者Java栈。

图2-5 线程专有的运行时数据区

图2-5展示了一个虚拟机实例的快照,它有3个线程正在执行。线程1和线程2都正在执行Java方法,而线程3则正在执行一个本地方法。在图5-3中,和本书其他地方一样,Java栈都是向下生长的,而栈顶都显示在图的底部,当前正在执行的方法的栈帧则以浅色表示,对于一个正在运行Java方法的线程而言,它的PC寄存器总是指向下一条将被执行的指令。在图2-5中,像这样的PC寄存器(比如线程1和线程2的)都是以浅色显示的。由于线程3当前正在执行一个本地方法,因此,它的PC寄存器(以深色显示的那个)的值是不确定的。

一个运行时的Java虚拟机实例的天职是:负责运行一个Java程序。在启动一个Java程序的同时会诞生一个虚拟机实例,当该程序退出时,虚拟机实例也随之消亡。如果在同一台计算机上同时运行3个Java程序,会得到3个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。

Java虚拟机实例通过调用某个初始类的main()方法来运行一个Java程序。而这个main()方法必须是公有的(public)、静态的(static)、返回值为void,并且接受一个字符串数组作为参数。任何拥有这样一个main()方法的类都可以作为Java程序运行的起点。假如存在这样一个Java程序,此程序能够打印出传给它的命令行参数:

package jvm.ext1;
public class Echo {
      public static void main(String[]args) {
            int length = args.length;
            for (int i = 0; i <length; i++) {
                  System.out.print(args[i] +"");
            }
            System.out.println();
      }
}

上述代码必须告诉Java虚拟机要运行的Java程序中初始类的名字,整个程序将从它的main()方法开始运行。现实中一个Java虚拟机实现的例子如SunJava 2 SDK的Java程序。比如,如果想要在Windows上使用Java运行Echo程序,需要键入如下命令。

java Echo Greeting, Planet

该命令的第一个单词“java”,告诉操作系统应该运行来自Sun Java 2 SDK的Java虚拟机。第二个词”Echo”则支持初始类的名字。Echo这个初始类中必须有个公有的、静态的方法main(),它获得一个字符串数组参数并且返回void。上述命令行中剩下的单词序列“Greeting,Planet”,作为该程序的命令行参数以字符串数组的形式传递给main(),因此,对于上面这个例子,传递给类Echo中main()方法的字符串数组参数的内容就是:

args[0]为”Greeting,”
args[1]为“Planet.”

Java程序初始类中的main()方法,将作为该程序初始线程的起点,任何其他的线程都是由这个初始线程启动的。

在Java虚拟机内部有两种线程:守护线程与非守护线程。守护线程通常是由虚拟机自己使用的,比如执行垃圾收集任务的线程。但是,Java程序也可以把它创建的任何线程标记为守护线程。而Java程序中的初始线程(即程序开始的main())是非守护线程。

只要还有任何非守护线程在运行,那么这个Java程序也在继续运行(虚拟机仍然存活)。当该程序中所有的非守护线程都终止时,虚拟机实例将自动退出。假若安全管理器允许,程序本身也能够通过调用Runtime类或者System类的exit方法来退出。

在上面的Echo程序中,方法main()并没有调用其他的线程。所以当它打印完命令行参数后返回main()方法。这就终止了该程序中唯一的非守护线程,最终导致虚拟机实例退出。

除了平台无关性以外,Java还必须解决的另一个技术难题就是安全。因为网络运行多台计算机共享数据和分布式处理,所以它提供了一条侵入计算机系统的潜在途径,使得其他人可能窃取信息、改变或破坏信息、盗取计算资源等。因此,将计算机联入网络产生了很多安全问题。为了解决由网络引起的安全问题,Java体系结构采用了一个扩展的内置安全模型,这个模型随着Java平台的主要版本而不断发展。在本节的内容中,将简要讲解JVM安全性的基本知识,为读者步入本书后面知识的学习打下基础。

Java安全模型侧主要用于保护终端用户免受从网络下载的、来自不可靠来源的、恶意程序的侵犯。为了达到这个目的,Java提供了一个用户可配置的“沙箱”,在沙箱中可以放置不可靠的Java程序。沙箱对不可靠程序的活动进行了限制,程序可以在沙箱的安全边界内做任何事,但是不能进行任何跨越这些边界的举动。例如,原来在版本1.0中的沙箱对很多不可靠Javaapplet的活动做了限制,主要包括:

由于下载的代码不可能进行这些特定的操作,这使得Java安全模型可以保护终端用户避免受到有漏洞的代码的威胁。在沙箱内有严格的限制,其安全模型甚至规定了对不可靠代码能做什么、不能做什么,所以用户可以比较安全地运行不可靠代码。但是对于1.0系统的程序员和用户来说,这个最初的沙箱限制太过严格,善意的代码常常无法进行有效的工作。所以在后来的1.1版本中,对最初的沙箱模型进行了改进,引入了基于代码签名和认证的信任模式。签名和认证使得接收端系统可以确认一系列class文件已经由某一实体进行了数字签名(有效,可被信赖),并且在经过签名处理以后,class文件没有改动。这使得终端用户和系统管理员减少了对某些代码在沙箱中的限制,但这些代码必须已由可信任团体进行数字签名。

虽然1.1版本的安全API包含了对认证的支持,但是其实只是提供了完全信任和完全不信任策略。Java 1.2提供的API可以帮助建立细粒度的安全策略,这种策略是建立在数字签名代码的认证基础上的。Java安全模型的发展经历了1.0版本的基本沙箱,然后是1.1版本的代码签名和认证,最后是1.2版以后的细粒度访问控制。

在计算机系统中,个人电脑中运行一个软件的前提是必须信任它。普通用户只能通过小心地使用来自可信任来源的软件来达到安全性,并且定期扫描,检查病毒来确保安全性。一旦某个软件有权使用我们的系统,那么它将拥有对这台电脑的完全控制权。如果这个软件是恶意的,那么它就可以为所欲为。所以在传统的安全模式中,必须想办法防止恶意代码有权使用你的计算机。

沙箱安全模型使得工作变得容易,即使某个软件来自我们不能完全信任的地方,通过沙箱模型可以使我们接受来自任何来源的代码,而不是要求用户避免将来自不信任站点的代码下载到机器上。当运行来自不可靠来源的代码时,沙箱会限制它进行任何可能破坏系统的动作指令。并且在整个过程中,无需指出哪些代码可以信任,哪些代码不可以信任,也不必扫描查找病毒。沙箱本身限制了下载的任何病毒或其他恶意的,有漏洞的代码,使得它们不能对计算机进行破坏。

如果你还有疑问,在确信它能保护你之前,用户必需确认沙箱没有任何漏洞。为了保证沙箱没有漏洞,Java安全模型对其体系结构的各方面都进行了考虑。如果在Java体系结构中有任何没有考虑到安全的区域,恶意的程序员很可能会利用这些区域来绕开沙箱。因此,为了对沙箱有一个了解,必须先看一下Java体系结构的几个不同部分,并且理解它们是怎样一起工作的。

下面列出了组成Java沙箱的基本组件。

Java的上述安全模型的前3个部分——类加载体系结构、class文件检验器、Java虚拟机(及语言)的安全特性一起达到一个共同的目的:保持JVM的实例和它正在运行的应用程序的内部完整性,使得它们不被下载的恶意代码或有漏洞的代码侵犯。相反,这个安全模型的第四个组成部分是安全管理器,它主要用于保护虚拟机的外部资源不被虚拟机内运行的恶意或有漏洞的代码侵犯。这个安全管理器是一个单独的对象,在运行的Java虚拟机中,它在对于外部资源的访问控制起中枢作用。

沙箱安全模型的最大优点之一是可以是用户自定义的,通过从Java1.1版本就已经引入的代码签名和认证技术,使正在运行的应用程序可以对代码区分不同的信任度。通过自定义沙箱,被信任的代码可以比不可靠的代码获得更多的访问系统资源的权限。这就防止了不可靠代码访问系统,但是却允许被信任的代码访问系统并进行工作。Java安全体系结构的真正好处在于,它可以对代码授予不同层次的信任度来部分地访问系统。

Microsoft提供了ActiveX控件认证技术,它和Java的认证技术相类似,但是ActiveX控件并不在沙箱中运行。这样使用了ActiveX,一系列移动代码要么是被完全信任的,要么是完全不被信任的。如果一个ActiveX控件不被信任,则它将被拒绝执行。虽然这对于没有认证来说是一个很大的提高,但是如果一些恶意的或是有漏洞的代码得到了认证,这段危险的代码将拥有对系统的完全访问权。Java的安全体系结构的优点之一就是,代码可以被授予只对它需要的资源进行访问的有限权限。即使一些恶意的或者有漏洞的代码得到了认证,它也很少有机会进行破坏。例如,一段恶意的或者有漏洞的代码可能只能删除一个固定目录下的为它设置的文件,而不是在本地硬盘上的所有文件。

从1.2版本开始的安全体系结构的主要目标是建立(以签名代码为基础的)细粒度的访问控制策略,这样不但过程更为简单而且更少出错。为了将不同的系统访问权限授予不同的代码单元,Java的访问控制机制必须能确认应该给每个代码段授予什么样的权限。为了使这个过程变得容易,载入1.2版本或其他虚拟机的每一个代码段(每个class文件)将和一个代码来源关联。代码来源主要说明了代码从哪里来,如果它被某个人签名担保的话,是从谁那里来。在1.2版本以后的安全模型中,权限(系统访问权限)是授给代码来源的。因此如果代码段请求访问一个特定的系统资源,只有当这个访问权限是和那段代码的代码来源相关联时,Java虚拟机才会把对那个资源的访问权限授予这段代码。

在1.2版本的安全体系结构中,对应于整个Java应用程序的一个访问控制策略是由抽象类java.security.Policy的一个子类的单个实例所表示的。在任何时候,每一个应用程序实际上都只有一个Policy对象。获得许可的代码可以用一个新的Policy对象替换当前的Policy对象,这是通过调用Policy.setPolicy()并把一个新的Policy对象的引用传递给它来实现的。类装载器利用这个Policy对象来帮助它们决定,在把一段代码导入虚拟机时应该给它们什么样的权限。

安全策略是一个从描述运行代码的属性集合到这段代码所拥有的权限的映射。在1.2版本的安全体系结构中,描述运行代码的属性被总称为代码来源。一个代码来源是由一个java.security.CodeSource对象表示的,这个对象中包含了一个java.net.URL,它表示代码库和代表了签名者的零个或多个证书对象的数组。证书对象是抽象类java.security.Certificate的子类的一个实例,一个Certificate对象抽象表示了从一个人到一个公钥的绑定,以及另一个为这个绑定作担保的人(以前提过的证书机构)。CodeSource对象包含了一个Certificate对象的数组,因为同一段代码可以被多个团体签名(担保)。这个签名通常是从Jar文件中获得的。

从1.2版本开始,所有和具体安全管理器有关的工具和访问控制体系结构都只能对证书起作用,而不能对公钥起作用。如果附近没有证书机构,可以用私钥对公钥签名,生成一个自签名的证书。当使用keytool程序生成密钥时,总是会产生一个自签名的证书。例如在上一节的签名例子中,keytool不仅产生了“公钥/私钥”对,而且还为别名friend和stranger产生了自签名的证书。

权限是用抽象类java.security.Permission的一个子类的实例表示的。一个Permission对象有3个属性,分别是类型、名字和可选的操作。权限的类型是由Permisstion类的名字指定的,例如java.io.FilePermission、java.net.SocketPermission和java.awt.AWTPermission。权限的名字是封装在Permission对象内的。例如某个FilePermission的名字可能是“/my/finances.dat”,某个SocketPermission的名字可能是“applets.artima.com:2000”,某个AWTPermission的名字可能是“showWindowWithoutBannerWarning”。Permission对象的第3个属性是它的动作。并不是所有的权限都有动作。例如,FilePermission的动作是“read, write”,SocketPermission的动作是“accept, connect”。如果一个FilePermission的名字为“/my/finances.dat”,并且有动作“read, write”,那么它就表示对文件“/my/finance.dat”可以进行读写操作。名字和动作都是由字符串来表示的。

Java API有一个很大的权限层次结构,在里面表示了所有可能潜在危险的操作。可以根据自己的目的创建自己的Permission类来表示自定义的权限,例如可以创建一个Permission类来表示对属性数据库的特定记录的访问权限。定义自定义的Permission类也是一种扩展版本1.2的安全机制类满足自己需要的方法。如果创建了自己的Permission类,可以像使用Java API中的Permission类一样来使用它们。

在Policy对象中,每一个CodeSource是和一个或多个Permission对象相关联的。和一个CodeSource相关联的Permission对象被封装在java.security.PermissionCollection的一个子类实例中。类装载器可以调用Policy.getPolicy()来获得一个当前有效的Policy对象的引用。然后它们可以调用Policy对象的getPermission()方法,传入一个CodeSource,从而得到和那个CodeSource对应的Permission对象的PermissionCollection。然后类装载器可以使用这个从Policy对象中得到的PermissionCollection来帮助判断应该给导入的代码授予什么权限。

长久以来,如何开发网络软件是Java开发人员所面临的最大挑战之一。在网络领域需要实现平台无关性,因为同一网络中通常连接了多种不同的计算机和设备。除此之外,安全模式也是一个挑战,因为网络可以方便地传输病毒和其他形式的恶意代码。在本节将详细讲解Java如何把握网络所带来的巨大机遇,为步入本书后面知识的学习打下基础。

当个人计算机互联成网变得越来越普遍的时候,另一种软件模式日益重要起来,即“客户机/服务器”模式。“客户机/服务器”模式将任务分为两部分,分别运行在两种计算机上:客户端进程运行在终端用户的个人计算机上,而服务器端进程运行在同一网络的另一台计算机上。客户端和服务器端的进程通过网络来回发送数据进行传输。服务器端进程通常只是简单地接收网络中客户端发来的数据请求命令,从中央数据库中提取需要的数据,并将该数据发送给客户端。而客户端在接到数据后,进行处理,然后显示并允许用户操作数据。这样的模式允许个人计算机的终端用户读取并操作放在中央储藏库的数据,而不需强迫这些用户共享中央CPU来处理数据。终端用户地区是共享了运行服务器端进程的CPU,但在一定程度上,数据处理是由客户端完成的,因此大大减轻了服务器端CPU的负载。

“客户机/服务器”模式最初被称作两层客户机/服务器模式,一层是客户端,另一层是服务器。更复杂一些的模型叫做3层(表示有3个进程)、4层(4个进程)或者N层结构,也就是说层次结构越来越多了。当更多的进程加入计算时,客户端和服务器的区别模糊了,于是人们开始使用“分布式处理”这个新名词来涵盖所有这些结构模式。

分布式处理模式综合了网络和处理器发展的优点,将进程分布在多个处理器上运行,并允许这些进程共享数据。尽管这种模式有许多大型计算机系统所无法比拟的优势,但它也有个不可忽视的缺点:分布式处理比大型计算机系统更难管理。在大型计算机系统中,软件应用程序存储在主机的磁盘上,虽然可以有多个用户使用该软件,但它只需在一个地方安装和维护。升级一个软件后,所有用户在下一次登录并启动该软件的时候可以得到这个新的版本。但是相反,在分布式系统中,不同组件的软件往往存储在不同的磁盘上,因此,系统管理员需要在分布式系统的不同组件上安装和维护软件。要升级一个软件时,管理员不得不分别升级每台计算机上的这个软件。所以,分布式处理的系统管理比大型计算机系统要困难得多。

Java的体系结构使软件的网络移动性成为可能,同时也预示了一种新的计算模式的到来。这种新的模式建立在流行的分布式处理模式的基础上,并可以将软件通过网络自动传送到各台计算机上。这样就解决了分布式处理系统中系统管理的困难。例如在一个C/S系统中,客户端软件可以存储在网络中的一台中央计算机上,当终端用户需要用该软件的时候,这个中央计算机会通过网络将可执行的软件传送到终端用户的计算机上运行。

因此,软件的网络移动性标志着计算模式发展历程中的重要一步,尤其是它解决了分布式处理系统中系统管理的问题,简化了将软件分布在多台CPU上的工作,使数据可以和相关软件一起被传送。

平台无关性使得在网络上传送程序更加容易,因为不需要为每个不同的主机平台都准备一个单独的版本,因此也不需要判断每台计算机需要哪个特定的版本,一个版本就可以对付所有的计算机。Java的安全特性促进了网络移动性的推广,因为最终用户就算从不信任的来源下载class文件,也可以充满自信。因此实际上,Java体系结构通过对平台无关性和安全性的支持,更好地推广了其class文件的网络机动性。

除了平台无关性和安全性之外,Java体系结构对网络移动性的支持主要集中在对在网络上传送程序的时间进行管理上。假若你在服务器上保存了一个程序,在需要的时候通过网络来下载它,这个过程一般都会比从本地执行该程序要慢。因此对于在网络上传送程序来说,网络移动性的一个主要难题就是时间。Java体系结构通过把传统的单一二进制可执行文件切割成小的二进制碎片——Javaclass文件——来解决这个问题。class文件可以独立在网络上传播,因为Java程序是动态链接、动态扩展的,最终用户不需要等待所有的程序class文件都下载完毕,就可以开始运行程序了。第一个class文件到手,程序就开始执行。class文件本身也被设计得很紧凑,所以它们可以在网络上飞快地传送。因此Java体系结构为网络移动性带来的直接主要好处就是把一个单一的大二进制文件分割成小的class文件,这些class文件可以按需装载。

Java应用程序从某个类的main()方法开始执行,其他的类在程序需要的时候才动态链接。如果某个类在一次操作中没有被用到,这个类就不会被装载。比如说,假若你在使用一个字处理程序,它有一个拼写检查器,但是在你使用的这次操作中没有使用拼写检查器,那么它就不会被装载。

除了动态链接之外,Java体系结构也允许动态扩展。动态扩展是装载class文件的另一种方式,可以延迟到Java应用程序运行时才装载。使用用户自定义的类装载器,或者Class类的forName()方法,Java程序可以在运行时装载额外的程序,这些程序就会变成运行程序的一部分。因此,动态链接和动态扩展给了Java程序员一些设计上的灵活性,既可以决定何时装载程序的class文件——而这又决定了最终用户需要等待多少时间来从网络上装载class文件。

除了动态连接和动态扩展,Java体系结构对网络移动性的直接支持还通过class文件格式体现。为了减少在网络上传送程序的时间,class文件被设计得很紧凑。它们包含的字节码流设计得特别紧凑——之所以被称为“字节码”,是因为每条指令都只占据一个字节。除了两个例外情况,所有的操作码和它们的操作数都是按照字节对齐的,这使得字节码流更小。这两个例外是这样一些操作码,在操作码和它们的操作数之间会填上1~3个字节,一边操作数都按照字边界对齐。

class文件的紧凑型隐含着另外一个含义,那就是Java编译器不会做太多的局部优化。因为二进制兼容性规则的存在,Java编译器不能做一些全局优化,比如把一个方法调用转化为整个方法的内嵌(内嵌指把被调用方法的整个方法体都替换到发起调用的方法中去,这样在代码运行的时候,可以节省方法调用和返回的时间)。二进制兼容性要求,假若一个方法被现有class文件包括以后,那么改变这个方法的时候必须不破坏已有的调用方法。在同一个类中使用的方法可能使用内嵌,但是一般来说,Java编译器不会做这种优化,部分原因是这样为class文件瘦身得不偿失。优化常常是在代码大小和执行速度间进行的折中。因此,Java编译器通常会把优化工作留给Java虚拟机,后者在装载类之后,在解释执行,即时编译或者自适应编译的时候都可以优化代码。

除了动态链接、动态扩展和紧凑的class文件之外,还有一些并非体系结构必须的策略,可以帮助控制在网络上传送class文件的时间。因为HTTP需要单独为Javaapplet中用到的每一个class文件请求连接,所以下载applet的很大一部分时间并不是用来实际传输class文件的时间,而是每一个class文件请求的网络协议握手的时间。一个文件需要的总时间是按照需要下载的class文件的数目倍增的。为了解决这个问题,Java 1.1包含了对Jar的支持,Jar文件允许在一次网络传输过程中传输多个文件,这和一次传送一个个单独class文件相比,大幅度降低了需要的总体下载时间。更大的优点是,Jar文件中的数据可以压缩,从而使下载时间更少。所以有时候通过一个大文件来传送软件。例如有些class文件是程序开始运行之前所必需的,这些文件可以很快地通过Jar文件一次性传递。

另外一个降低最终用户等待时间的策略就是不采取按需下载class文件的做法。有几种不同的技术,例如MarimbaCastanet使用的订阅模式,可以在需要class文件之前就已经把它们下载下来了,这样程序就可以更快地启动。

因此,除了平台无关性和安全性能够对网络移动性有利外,Java体系结构的主要着眼点就是控制class文件在网络上传送的时间。动态链接和动态扩展允许Java程序按照小功能单元设计,在最终用户需要的时候才单独下载。Class文件的紧凑性本身有助于减少Java程序在网络上传送的时间。Jar文件允许在一次网络连接中传送多个文件,还允许数据压缩。

对于C和C++的开发人员来说,在内存管理领域应该能够游刃有余。在计算机系统中,内存负责维护每一个对象生命的从开始到终结。Java内存分配与管理是Java的核心技术之一,通常Java在内存分配时会涉及到以下区域。

1.栈

在函数中定义的一些基本类型的变量数据,还有对象的引用变量都在函数的栈内存中分配。当在一段代码块中定义一个变量时,Java就在栈中为这个变量分配内存空间;当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

栈也称为栈内存,是Java程序的运行区,是在线程创建时创建,它的生命期跟随着线程的生命期,线程结束栈内存也就释放。对于栈来说不存在垃圾回收问题,只要线程一结束,该栈就被释放。问题出来了:栈中存的是那些数据呢?又什么是格式呢?

栈中的数据都是以栈帧(Stack Frame)的格式存在,栈帧是一个内存区块,是一个数据集,是一个有关方法(Method)和运行期数据的数据集。当一个方法A被调用时就产生了一个栈帧F1,并被压入到栈中,A方法又调用了B方法,于是产生栈帧F2也被压入栈;执行完毕后,先弹出F2栈帧,再弹出F1栈帧,遵循“先进后出”原则。

那栈帧中到底存在着什么数据呢?在栈帧中主要保存如下3类数据。

光说比较枯燥,画个图来理解一下Java栈,如图2-6所示。

在图2-6中,一个栈中有两个栈帧,栈帧2是最先被调用的方法,先入栈,然后方法2又调用了方法1。栈帧1处于栈顶的位置,栈帧2处于栈底,执行完毕后,依次弹出栈帧1和栈帧2,线程结束,栈释放。

图2-6 Java栈

2.堆

堆内存用来存放由关键字new创建的对象和数组。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理。

在堆中产生了一个数组或对象后,还可以在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。引用变量就相当于是为数组或对象起的一个名称,以后就可以在程序中使用栈中的引用变量来访问堆中的数组或对象。引用变量就相当于是为数组或者对象起的一个名称。

引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序运行到使用new产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能再被使用,但仍然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。这也是 Java 比较占内存的原因。

实际上,栈中的变量指向堆内存中的变量,这就是Java中的指针。

3.常量池(constant pool)

常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:

虚拟机必须为每个被装载的类型维护一个常量池。常量池就是该类型所用到常量的一个有序集合,包括直接常量(string, integer和floating point常量)和对其他类型、字段和方法的符号引用。

对于String常量,它的值是在常量池中的。而JVM中的常量池在内存当中是以表的形式存在的,对于String类型,有一张固定长度的CONSTANT_String_info表用来存储文字字符串值,但是该表只存储文字字符串值,并不存储符号引用。在程序执行的时候,常量池会储存在Method Area(方法区域)中,而不是堆中。

一个JVM实例只存在一个堆内存,堆内存的大小是可以调节的。类加载器读取了类文件后,需要把类、方法、常变量放到堆内存中,以方便执行器执行。堆内存分为3部分。

(1)永久存储区(Permanent Space)。

永久存储区是一个常驻内存区域,用于存放JDK自身所携带的Class Interface的元数据。也就是说,它存储的是运行环境必须的类信息,被装载进此区域的数据是不会被垃圾回收器回收掉的,关闭JVM才会释放此区域所占用的内存。

(2)新生区(Young Generation Space)。

新生区是类的诞生、成长、消亡的区域,一个类在这里产生、应用,最后被垃圾回收器收集,结束生命。新生区又分为伊甸区(Eden space)和幸存者区(Survivor pace)两部分。所有的类都是在伊甸区被new(新建)出来的;幸存区有两个:0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收,将伊甸园区中不再被其他对象所引用的对象进行销毁,然后将伊甸园中的剩余对象移动到幸存0区。若幸存0区也满了,再对该区进行垃圾回收,然后移动到1区。那如果1区也满了呢?再移动到养老区。

(3)养老区(Tenure Generation Space)。

养老区用于保存从新生区筛选出来的Java对象,一般池对象都在这个区域活跃。

上述3个区的示意图如图2-7所示。

图2-7 堆内存的3个区

Java通过自身的动态内存分配和垃圾回收机制,可以使Java程序员不用像C++程序员那么头疼内存的分配与回收。对于这一点来说,相信熟悉COM机制的朋友对于引用计数管理内存的方式深有感触。通过Java虚拟机的自动内存管理机制,不仅降低了编码的难度,而且不容易出现内存泄露和内存溢出的问题。但是这过于理想的愿望正是由于把内存的控制权交给了Java虚拟机,一旦出现内存泄露和溢出,我们就必须翻过Java虚拟机自动内存管理这堵高墙去排查错误。

根据《Java虚拟机规范》的规定,Java虚拟机在执行Java程序时,即运行环境下会把其所管理的内存划分为几个不同的数据区域。有的区域伴随虚拟机进程的启动而创建,死亡而销毁;有些区域则是依赖用户线程的启动时创建,结束时销毁。所有线程共享方法区和堆,虚拟机栈、本地方法栈和程序计数器是线程隔离的数据区。Java虚拟机运行时的数据区结构如图2-8所示。

图2-8 Java虚拟机运行时的数据区结构

JVM的逻辑内存模型如图2-9所示。

图2-9 JVM的逻辑内存模型

当建立一个对象时如何进行访问呢?在Java 语言中,对象访问是如何进行的?对象访问在Java语言中无处不在,是最普通的程序行为,但即使是最简单的访问,也会涉及Java栈、Java 堆、方法区这3个最重要内存区域之间的关联关系,如下面的代码:

Object obj = new Object();

假设这句代码出现在方法体中,那“Object obj”这部分的语义将会反映到Java栈的本地变量表中,作为一个reference类型数据出现。而“new Object()”这部分的语义将会反映到Java堆中,形成一块存储了Object类型所有实例数据值(Instance Data,对象中各个实例字段的数据)的结构化内存,根据具体类型以及虚拟机实现的对象内存布局(Object Memory Layout)的不同,这块内存的长度是不固定的。另外,在Java堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现的接口、方法等)的地址信息,这些类型数据则存储在方法区中。

由于reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过哪种方式去定位,以及访问到Java堆中的对象的具体位置,因此不同虚拟机实现的对象访问方式会有所不同,主流的访问方式有使用句柄和直接指针两种。

(1)如果使用句柄访问方式,Java 堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息,如图2-10所示。

(2)如果使用直接指针访问方式,Java 堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中直接存储的就是对象地址,如图2-11所示。

这两种对象的访问方式各有优势,使用句柄访问方式的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference 本身不需要被修改。

图2-10 用句柄访问对象

图2-11 通过指针访问对象

使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本。从整个软件开发的范围来看,各种语言和框架使用句柄来访问的情况也十分常见。

在计算机科学中,内存泄漏(Memory Leak)是指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。内存泄漏与许多其他问题有着相似的症状,并且通常情况下只能由那些可以获得程序源代码的程序员才可以分析出来。然而,有不少人习惯于把任何不需要的内存使用的增加描述为内存泄漏,严格意义上来说这是不准确的。一般常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的、大小任意的(内存块的大小可以在程序运行期决定)、使用完后必须显式释放的内存。应用程序一般使用malloc、realloc、new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则这块内存就不能被再次使用,一般就说这块内存泄漏了。

通常可以将内存泄露分为以下4类。

(1)常发性内存泄漏。

发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。

(2)偶发性内存泄漏。

发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生,常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。因此测试环境和测试方法对检测内存泄漏至关重要。

(3)一次性内存泄漏。

发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅一块内存发生泄漏。比如,在一个Singleton类的构造函数中分配内存,在析构函数中却没有释放该内存。而Singleton类只存在一个实例,所以内存泄漏只会发生一次。

(4)隐式内存泄漏。

程序在运行过程中不停地分配内存,但是直到结束的时候才释放内存。严格地说,这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天、几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。因此,称这类内存泄漏为隐式内存泄漏。

GC执行时要耗费一定的CPU资源和时间,因此在JDK1.2以后,JVM引入了分代收集的策略,其中对新生代采用“Mark-Compact“策略,而对老生代采用了“Mark-Sweep"的策略。其中新生代的垃圾收集器命名为“minor gc”,老生代的GC命名为“Full Gc”或“Major GC”。其中用System.gc()强制执行的是Full Gc。

1.Serial Collector

Serial Collector是指任何时刻都只有一个线程进行垃圾收集,这种策略有一个名字“stop the whole world”,它需要停止整个应用的执行。这种类型的收集器适合于单CPU的机器。

Serial Copying Collector

此种GC用-XX:UseSerialGC选项配置,它只用于新生代对象的收集。1.5.0以后-XX:Max TenuringThreshold来设置对象复制的次数。当eden空间不够时,GC会将eden的活跃对象和一个名叫From survivor空间中尚不够资格放入Old代的对象复制到另外一个名字叫To Survivor的空间。而此参数就是用来说明到底From survivor中的哪些对象不够资格,假如这个参数设置为31,那么也就是说只有对象复制31次以后才算是有资格的对象。

From Survivor和To Survivor的角色是不断变化的,同一时间只有一块空间处于使用状态,这个空间就叫做From Survivor区,当复制一次后角色就发生了变化。

如果复制的过程中发现To Survivor空间已经满了,那么就直接复制到Old Generation。

比较大的对象也会直接复制到Old Generation,在开发中,应该尽量避免这种情况的发生:

Serial  Mark-Compact Collector

串行的标记-整理收集器是JDK5 update 6之前默认的老生代的垃圾收集器,此收集使得内存碎片最少化,但是它需要暂停的时间比较长

2.Parallel Collector

Parallel Collector主要是为了应对多CPU,大数据量的环境。Parallel Collector又可以分为以下两种。

(1)Parallel Copying Collector:此种GC用-XX:UseParNewGC参数配置,它主要用于新生代的收集,此GC可以配合CMS一起使用。

(2)在1.4.1版本以后用:

Parallel Mark-Compact Collector

此种GC用-XX:UseParallelOldGC参数配置,此GC主要用于老生代对象的收集。1.6.0后用:

Parallel scavenging Collector

此种GC用-XX:UseParallelGC参数配置,它是对新生代对象的垃圾收集器,但是它不能和CMS配合使用,它适合于比较大新生代的情况,此收集器起始于jdk 1.4.0。它比较适合于对吞吐量高于暂停时间的场合。

3.Concurrent Collector

Concurrent Collector通过并行的方式进行垃圾收集,这样就减少了垃圾收集器收集一次的时间,这种GC在实时性要求高于吞吐量的时候比较有用。此种GC可以用参数-XX:UseConcMarkSweepGC配置,此GC主要用于老生代和Perm代的收集。

如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。这里讨论的收集器基于Sun HotSpot虚拟机1.6版 Update 22,这个虚拟机包含的所有收集器如图2-12所示。

图2-12 HotSpot JVM 1.6的垃圾收集器

图2-12展示了7种作用于不同分代的收集器(包括JDK 1.6_Update14后引入的Early Access版G1收集器),如果两个收集器之间存在连线,就说明它们可以搭配使用。

在介绍这些收集器各自的特性之前,先来明确一个观点:虽然是在对各个收集器进行比较,但并非为了挑选一个最好的收集器出来。因为直到现在为止还没有最好的收集器出现,更加没有万能的收集器,所以本书选择的只是对具体应用最合适的收集器。这点不需要多加解释就能证明:如果有一种放之四海皆准、任何场景下都适用的完美收集器存在,那HotSpot虚拟机就没必要实现那么多不同的收集器了。

不同的平台,内存模型是不一样的,但是JVM的内存模型规范是统一的。其实Java的多线程并发问题最终都会反映在Java内存模型上,所谓线程安全无非是要控制多个线程对某个资源的有序访问或修改。总结Java的内存模型,要解决两个主要的问题:可见性和有序性。

人们都知道计算机有高速缓存的存在,处理器并不是每次处理数据都是取内存的。JVM定义了自己的内存模型,屏蔽了底层平台内存管理细节,对于Java开发人员,要清楚在JVM内存模型的基础上,如果解决多线程的可见性和有序性。

那么,何谓可见性?多个线程之间是不能互相传递数据通信的,它们之间的沟通只能通过共享变量来进行。Java内存模型(JMM)规定了JVM有主内存,主内存是多个线程共享的。当新建一个对象的时候,也是被分配在主内存中,每个线程都有自己的工作内存,工作内存存储了主存的某些对象的副本,当然线程的工作内存大小是有限制的。当线程操作某个对象时,执行顺序如下。

(1)从主存复制变量到当前工作内存(read and load)。

(2)执行代码,改变共享变量值(use and assign)。

(3)用工作内存数据刷新主存相关内容(store and write)。

Java平台自动集成了线程以及多处理器技术,这种集成程度比Java以前诞生的计算机语言要厉害很多。该语言针对多种异构平台的平台独立性而使用的多线程技术支持也是具有开拓性的一面,有时候在开发Java同步和线程安全要求很严格的程序时,往往容易混淆的一个概念就是内存模型。究竟什么是内存模型?内存模型描述了程序中各个变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存中取出变量这样的底层细节,对象最终是存储在内存里面的,这点没有错,但是编译器、运行库、处理器或者系统缓存可以有特权在变量指定内存位置存储或者取出变量的值。

JVM规范定义了线程对主存的操作指令:read、load、use、assign、store、write。当一个共享变量在多个线程的工作内存中都有副本时,如果一个线程修改了这个共享变量,那么其他线程应该能够看到这个被修改后的值,这就是多线程的可见性问题。那么,什么是有序性呢?线程在引用变量时不能直接从主内存中引用,如果线程工作内存中没有该变量,则会从主内存中复制一个副本到工作内存中,这个过程为read-load,完成后线程会引用该副本。当同一线程再度引用该字段时,有可能重新从主存中获取变量副本(read-load-use),也有可能直接引用原来的副本(use),也就是说read、load、use顺序可以由JVM实现系统决定。

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。此处的变量(Variable)与Java编程中所说的变量略有区别,它包括了实例字段、静态字段和构成数组对象的元素,但是不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不存在竞争问题。为了获得较好的执行效能,Java内存模型并没有限制执行引擎使用处理器的特定寄存器或缓存来与主内存进行交互,也没有限制即时编译器调整代码执行顺序这类权利。

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件时的主内存名字一样,两者也可以互相类比,但此处仅是虚拟机内存的一部分)。每条线程还有自己的工作内存(Working Memory,可与前面所讲的处理器高速缓存类比),线程的工作内存中保存了被该线程使用到的变量的主内存副本复制,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,线程、主内存、工作内存三者的交互关系如图2-13所示。

图2-13 线程、主内存和工作内存之间的交互关系

关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,在Java内存模型中定义了以下8种操作来完成内存间的交互操作。

如果要把一个变量从主内存复制到工作内存,那就要按顺序地执行read和load操作;如果要把变量从工作内存同步回主内存,就要按顺序地执行store和write操作。注意,Java内存模型只要求上述两个操作必须按顺序执行,而没有保证必须是连续执行。也就是说read与load之间、store与write之间是可插入其他指令的,如对主内存中的变量a、b进行访问时,一种可能出现的顺序是read a、read b、load b、load a。除此之外,Java内存模型还规定了在执行上述8种基本操作时必须满足如下规则。


相关图书

Android App开发入门与实战
Android App开发入门与实战
Kotlin入门与实战
Kotlin入门与实战
Android 并发开发
Android 并发开发
Android APP开发实战——从规划到上线全程详解
Android APP开发实战——从规划到上线全程详解
Android应用案例开发大全( 第4版)
Android应用案例开发大全( 第4版)
深入理解Android内核设计思想(第2版)(上下册)
深入理解Android内核设计思想(第2版)(上下册)

相关文章

相关课程