1. 默认情况下,每个应用程序均运行于它自己的linux进程中。
每个进程都运行于自己的java虚拟机(VM)中。所以应用程序代码实际上与其他的应用程序的代码是隔绝的。
默认情况下,每个应用程序均被赋予一个唯一的linux用户ID,并加以权限设置,使得应用程序的文件仅对这个用户,这个应用可见。当然,也有其他办法使得这些文件对其他应用程序可见。
使两个应用程序共有同一个用户ID是可行的,这中情况下他们可以看到自己彼此的文件。从系统资源维护的角度来看,用有同一个ID的应用程序也在运行时使用同一个linux进程,以及同一个虚拟机。
二. android体系结构
android 体系结构从静态和动态两种视角来认识,
一。静态视角
android采用分层的体系结构,从上到下,分别为:应用层,应用框架层,android运行环境和系统运行库层, linux内核层。
(1)应用层
位于android体系结构的最上层,Google在android中内置了一些应用程序,如主屏幕(home),联系人(Contacts), 电话(Phone)等。
主要用java语言编写,从Android 1.5 开始,Google提供了NDK开发工具,可以方便地开发基于JNI的应用程序。NDK便于开发者开发需要基于C/C++才能实现的功能呢,也可以提高程序执行效率。
(2)应用框架层
为应用层提供API,且是一种重要的机制。这种机制为应用层提供了可以使用的组件,提供了应用层开发的规范,屏蔽了应用层与底层交互的复杂性。
应用框架层提供的API并不完全对第三方应用程序开放,有一部分是隐藏的。
本层主要使用java和JNI实现,位于该层的主要组件如下:
(3)android运行环境和系统运行库层
相当于中间件层,
为应用框架层提供服务,它分成两个部分:
一部分是系统运行库,包含各种系统库和第三方库;
另一部分是android运行环境,这里主要是使用C++和C实现的。
应用框架层为应用层提供的功能,在底层大多是由系统运行库实现的。
android应用层使用的多媒体,浏览器,数据库,图形引擎等,其功能实现均位于该层。
运行库的主要组件:
android运行环境的主要组件:
(4)linux内核层
Android自ICS开始基于Linux 3.0内核,充分利用了Linux内核基于权限的安全模型,内存管理,进程管理,网络协议栈和驱动模型等优点,并在Lower Memory Killer,进程间通信(Binder),电源管理以及日志系统(Logger)等方面引入了不同与标准Linux的全新实现。
android对标准linux内核做了大量剪裁和优化,其修改主要集中在一下几方面:
>弃用标准linux的GUI系统
>此应用更有效率的Bionic Libc库代替glibc库
>基于ARM架构增加了Gold-Flsh平台
>专有的驱动程序:Binder, Logger, PowerManager, Timed GPIO, Alarm, Ashmem, RAM Console
linux遵守GPL license, android遵守Apache license。为了保证GPL license完全开放源码的规定,保护硬件厂商的驱动程序,android把控制硬件的操作放到了android HAL(硬件抽象层)中,在内核中只有简单的读写寄存器的操作。遵守Apache license ,硬件厂商可以值提供二进制代码,而不需要提供源码。 HAL层并没有在官方体系结构中体现出来,实际上他位于linux内核层与android运行环境和系统运行库层之间。
注意:引入HAL层,可以使android和linux的耦合度更低,减少android对linux内核和驱动的依赖,方便系统移植和接口开发。
区分应用框架层,框架层和frameworks的概念:
》
应用框架层:特指android四层体系结构中的Application Framework。
应用框架层不仅为应用层提供API和UI控件,而且为应用层提供了一套代码设计的模式。引入应用框架层后,应用层被浓缩为Activity, Service, Content Provider 和 Broadcast Receiver四大组件。
》
框架层: 严格说并没有一个明确的界限去定义框架层。从android体系结构的视角,可以把应用框架层,android运行环境和系统运行库层以及Binder都归入框架层的范畴。从代码的角度,可以把dalvik, frameworks, external, libcore, system这几个包的内容归入框架层的范畴。
》framework:一般指android源码中的frameworks包
二。动态视角
将android划分为两个交互的空间:
用户空间和内核空间
(1)用户空间
用户空间分为两个交互的子系统:
Native子系统和
java子系统。两个子系统
通过JNI技术连接在一起,建立在内核空间之上。
Native子系统主要有NDK开发的App, 应用框架层Native部分和标准库C/C++部分组成。
Native子系统主要有NDK开发的App, 应用框架层Native部分和标准库C/C++部分组成。
JAVA子系统主要由SDK开发的APP,系统内置APP,应用程序框架层java部分,标准库的java接口组成
运行时,java应用程序调用应用框架层的接口使用标准库提供的服务。整个系统的运行环境和进程管理便由Dalvik和linux Kernel负责。
(2)内核空间
内核空间分为两部分:
linux内核和android扩展支持,用于完成操作系统运行支持。其中android扩展部分包括Binder, Logger, OOM等机制。
android源码下载
git是为了管理linux内核开发而开发的一个开源分布式版本管理软件,它与SVN, CVS这样的集中式八本管理软件有很大的不同。
集中式管理软件中多个客户端共享一个仓库(repository),而在git这样的分布版本管理软件中,每一个客户端都包含一个完整的仓库,客户端可以离线操作,本地提交可以稍后在提交到服务器上。
android是由kernel, dalvik , bionic , prebuilt , frameworks等多个git库组成,为了方便使用,android项目提供了一个名为repo的Python的脚本来统一管理这些git 仓库。
android源码分为两部分,其中kernel部分需要单独下载。上层代码需要单独下载。
3. 源码下载:
(1) 》 建立repo工作目录:
$mkdir ~/bin #在主目录下创建bin目录
$PATH=~/bin:$PATH #将bin目录加入PATH环境变量
》 下载repo脚本
$curl
http://dl-ssl.google.com/dl/googlesource/git-repo/repo > ~/bin/repo #下载repo脚本到bin目录
$chmod a+x ~/bin/repo #给repo脚本可执行权限
》 建立android源码目录
$mkdir -p ~/android/jellybean #建立jellybean目录存放android 4.1源代码
$cd ~/android/jellybean
》 初始化repo
allong@android:~/android/jellybean $ repo init
-u
http://android.googlesource.com/platform/manifest -b android-4.1.1_r3
#其中-u为源码的git服务器地址,-b为源码的某个分支。
注意: 若不懂源码服务器上的分支情况,可以执行“git ls-remote” 命令查看远程服务器都有哪些分支,然后选择新的分支下载。
$git ls-remote -tags
http://android.googlesource.com/platform/manifest
执行结果如下:
#可根据tags后的值判断有哪些branch可供下载。
注意: refs包含heads和tags两个子目录,其中存放了不同分支的头的索引。可以通过索引查看有哪些branch。若没有指定-b,将下载主线(master默认分支)上最新版本源码,不过这部分代码不稳定。
》下载android源码
$repo sync
(2)下载指定模块源码
查看有哪些模块可以下载,在终端中执行: repo manifest -o - 并执行 “ repo sync+ 路径名称 ”即可下载模块信息
执行后,将显示如下信息:
其中,name表示项目模块的名称以及在源码服务器上的相对路径,path表示项目的本地路径。
该命令读取的是本地源码根目录下的.repo/manifests/default.xml文件,读者可直接打开该文件,也可得到同样的项目信息。
(3)下载android linux Kernel源码
Kernel部分的源码没有采用repo工具管理,可以直接通过git工具下载,步骤如下:
》进入android源码根目录。建立kernel目录
$mkdir kernel -> $cd kernel
》下载kernel源码
可在终端中执行任意一条命令,下载android Kernel部分源码。
》检出Kernel3.0分支
$cd common #进入common版内核的下载路径
$git branch -a #查看都有哪些分支
$git checkout remotes/origin/Android-3.0 #检出kernel 3.0分支
(4)编译android上层系统源码
编译流程:
》导入预设脚本。在终端执行命令: $ . build/envsetup.sh
注:“.”后面有空格,“.”在shell中是执行,使用方式“. filename”,作用是从filename中读取指令并执行。也可以用 source build/envsetup.sh 代替。
》执行产品名和编译变量。在终端输入: $ lunch
出现如下情况:
注意: lunch是envsetp.sh脚本中提供的函数,负责设置一些环境变量,比如 TARGET_PRODUCT, TARGET_BUILD_VARIANT等。
full 表示完全编译 eng 表示工程版 full-eng对应模拟器设备
》编译全部源码,在终端执行命令: $make -j8 #开启8线程开始编译
(5)编译指定模块
三种用于编译单独模块的方式:
》make模块名
》mm 来自与envsetup.sh脚本中注册的函数
》mmm来自于envsetup.sh脚本中注册的函数
1. make模块名
此方式使用与第一次编译,会把依赖的模块一并编译。它需要在全部源代码中找到编译模块的Android.mk文件,并检查依赖模块是否修改。使用此方式,我们只需要搜素源码目录下的Android.mk文件,找到模块名,然后指定给make即可。
(1)编译应用层源码
对于应用层程序,需要查看Android.mk文件的LOCAL_PACKAGE_NAME变量。找到其所指的字段,此字段就编程了我们编译的参数。
例如,在Android.mk文件中有: LOCAL_PACKAGE_NAME:=Phone
此时就可以单独编译Phone模块及其依赖模块: $ make Phone
(2)编译框架层和系统运行库源码
对于框架层和系统运行库,需要查看LOCAL_MODULE变量
在Andorid.mk文件中通常会指定LOCAL_MODULE变量的名称,例如: LOCAL_MODULE:=app_process
此时这个变量的值就是我们要找的模块名,在终端运行: $make app_process
2. mmm命令
此命令是在envsetup.sh 中注册的函数,用于在源码根目录编译指定模块,参数为模块的相对路径。只能在第一次编译后使用。
3. mm命令
同上出自envsetup.sh文件中,用于在模块根目录编译这个模块。只能在第一次编译后使用。
注意:mmm和mm命令必须在执行“.build/envsetup.sh”之后才能使用,并且只编译发生变化的文件。如果要编译模块的所有文件,需要加“-B”选项。
(6)android源码结构
在终端执行命令: $tree -L 1 即可得到源码的树形结构。下表源码文件的说明。
应用层源码位于packages目录下,主要包含核心应用程序,内容提供器(provider), 输入法等;
应用程序框架层源码位于frameworks目录下;
系统运行库分布于bionic, external等目录下;
android 核心库位于libcore目录下;
dalvik是android虚拟机的源码目录;
其他目录主要是编译和开发工具的源码目录
三。 框架基础JNI
JNI(java Native Interface Java本地接口)是java平台上定义的一套标准的本地编程接口。允许java代码与本地代码互操作,即java代码可以调用本地代码,本地代码也可以调用java代码。所谓的本地代码指的是用其他语言(C/C++)实现的,依赖于特定硬件和操作系统的代码。通过JNI 调用本地代码,可以实现java语言所不能实现的功能。在Android 平台上,Dalvik虚拟机会实现JNI定义的接口。
1. JNI在android系统中所处的位置
android采用分层结构:上层的应用层和应用框架层主要使用java语言开发;下层则运行一个linux内核,并在内核之上集成了各种核心库和第三方库,以提供系统运行所需的服务,这部分是由C和C++语言实现的。连接这两部分的纽带就是JNI.
JNI在android系统中所处的位置如下:
如上,
JNI可直接调用本地代码库,并可以通过虚拟机实现与应用层和应用框架层的交互。
Android JNI部分的代码主要位于Android体系结构中的上面两层:
》应用层:采用NDK开发,主要使用标准JNI编程模型实现
》应用框架层:android定义了一套JNI编程模型,使用函数注册方式弥补了标准JNI编程模型的不足。
android应用框架层JNI部分源码主要位于 frameworks/base/目录下。按模块组织,不同的模块将被编程为不同的共享库,分别为上层提供不同的服务。这些共享库最终会被放到system/lib目录下。
注意: NDK与JNI的区别: NDK是为了便于开发基于JNI的应用而提供的一套开发和编译工具集;而JNI则是一套编程接口,可以运用在应用层,也可以运用在应用框架层,以实现java代码和本地代码的互操作。
JNI 编程模型的结构,可概括为如下步骤:
a . java层声明Native方法。
b . JNI层实现java层声明的Native方法,在JNI层可以调用底层库或者回调java层方法。这部分将被编程为动态库供系统加载。
c . 加载JNI层代码编译后生成的共享库。
注意:JNI中的部分概念:
native: 特指java语言中的方法修饰符native
Native方法: 特指java层中声明的,用native修饰的方法
JNI层: 特指采用JNI技术实现java层声明的Native方法的部分。
JNI函数: 特指JNIEnv提供的函数
JNI方法: 特指Native方法对应的JNI层实现方法。
2. JNI框架层实例分析
框架层大量使用了JNI技术来完成对系统运行库的调用。
一般使用应用框架层的android.util.Log提供的java接口来使用日志系统。这个接口其实通过JNI调用系统运行库并最终调用内核驱动程序Logger把Log写到内核空间中的。
Log在系统中的实现如下列文件:
(1)Log系统java层分析
Log系统java层部分为Log.java文件,在该文件中只定义了isLoggable和println_native两个Native方法。
只声明为native而无需实现,就可直接调用,不会出现任何编译错误。
(2) Log系统的JNI层
JNI层实现java层声明的Native方法。对于Log类,其对应的JNI文件是android_util_Log.cpp
从代码中可以看出,JNI层的实现方法只是根据一定的规则与java层声明的方法做了一个映射,然后通过本地库函数或JNIEnv提供的JNI函数响应java层调用。
(3)Log系统的JNI方法注册
JNI层已经实现了java层的Native方法。这两个方法的连接实现在android_util_Log.cpp文件中。
此处的gMethods数组用来存储JNINativeMethod类型的数据,而这个类型的定义在jni.h文件中:
数组中保存了函数和实现函数的意义对应关系。其中每一个成员的信息分析如下:
》 java层声明的Native函数名为isLoggable。
》 java层声明的Native函数的签名为(Ljava/lang/String;I)Z
》 JNI层实现方法的指针为 (void*)android_util_Log_isLoggable
这样就得到了java层方法和JNI层方法的对应关系。然后在android_util_Log.cpp 中有如下代码:
通过调用AndroidRuntime::registerNativeMethods方法,将gMethods数组,java层类名以及一个JNIEnv类型的指针一同传给
registerNativeMethods。此方法最终调用JNIEnv的RegisterNatives函数,将gMethods中存储的方法关联信息传递给虚拟机。
此方法是向clazz参数指定的类注册本地方法。这样虚拟机就得到了java层和JNI层之间的对应关系,可以是实现java和C/C++代码的交互。
注意:
(1)JNIEnv 是一个指针,指向了一组JNI函数,通过这组函数可以在JNI层操作java对象,以层此实现java层和native交互。
(2)register_android_util_Log函数是指在系统启动过程中通过AndroidRuntime.cpp的register_jni_procs方法执行的。通过调用register_android_util_Log将函数的映射关系注册到Dalvik虚拟机的。
(3)使用JNI的有两种方式:一种是遵守JNI规范的函数命令规范,建立声明函数和实现函数之间的对应关系;另一种是Log系统中采用的函数注册方式。应用层多采用第一种,应用框架层多采用第二种。
3. JNI总管:JNIEnv
在log系统的实例中,JNI层实现方法和注册方法中都使用了JNIEnv这个指针,通过它调用JNI函数,访问java虚拟机,进而操作java对象。下面介绍JNIEnv的体系结构。
图中,JNIEnv首先指向一个线程相关的结构,该结构又指向一个指针数组,而这个指针数组中的每个元素最终指向一个JNI函数。所以可以通过JNIEnv去调用JNI函数。
C++中: JNIEnv就是 struct _JNIEnv。 JNIEnv * env等价于struct _JNIEnv *env,在调用JNI函数的时候,只需要
env->FindClass(JNIEnv*, const char*),就会间接地调用JNINativeInterface结构体里定义的函数指针,而无需首先对env解引用。
C中: JNIEnv就是 const struct JNINativeInterface*。JNIEnv* env实际等价于const struct JNINativeInterface ** env ,因此要得到JNINativeInterface结构体内的函数指针就必须先对env解引用得到(*env),这个指针才能真正指向JNINativeInterface结构体的指针,然后通过它调用具体的JNI函数。因此需要这样diaoyong: (*env)->FindClass(JNIEnv*, const char* )
注意:
JNIEnv只在当前线程中有效。本地方法不能将JNIEnv从一个线程传递到另一个线程。相同的java线程中对本地方法多次调用时,传递给本地方法的JNIEnv 是相同的。但一个本地方法可以被不同的java线程多调用,因此可以接受不同的JNIEnv。
4. 在java中调用JNI实现方法
(1) java数据类型与JNI数据类型转换
java中调用Native方法传递的参数是java类型的,这些参数要经过Dalvik虚拟机转化为JNI类型才能被JNI层识别。下面是基本数据类型与引用类型的转换关系:
JNI的引用类型定义了9中数组类型,以及jobject, jclass , jstring, jthrowable四种类型。他们的继承关系如下:
(2)JNI方法命名规则
Log系统中,JNI实现方法与java声明方法是不同的,java层声明的Native方法名为 isLoggable,而对应的JNI实现方法的方法名却是 android_util_Log_isLoggable。可见,除了数据类型有对应关系外,方法名也有对应关系。
JNI接口指针是JNI实现方法的第一个参数,其类型是JNIEnv。第二个参数因本地方法是静态还是非静态而有所不同。非静态本地方法的第二个参数是对java对象的引用,而静态本地方法的第二个参数是对其java类的引用。其余的参数都对应于java方法的参数。
JNI规范里提供了JNI 实现方法的命名规则,方法名由一下几部分串接而成:
java_前缀
全局限定的类名
下划线分隔符
增加第一个参数JNIEnv*env
增加第二个参数jobject
其他参数按类型映射
返回类型映射
例如,Log系统中,java方法声明为: public static native boolean isLoggable(String tag, int level);
而JNI部分方法实现为: static jboolean android_util_Log_isLoggable(JNIEnv* env, jobject clazz, jstring tag, jint level){......}
(3)JNI方法签名规则
有了对应关系,JNI就可以正确识别并转换java类型,但是java方法支持重载,仅靠函数名无法确定唯一方法,于是JNI提供了一套签名规则,用一个字符串来唯一确定一个方法。规则如下:
(参数1类型签名参数2类型签名。。。。。。参数n类型签名)返回值类型签名
以上签名字符串之间没有空格。
注意: 类的签名规则是:“L+全限定类名+;”三部分组成,其中全限定类名以“/”分割。
例如: java方法: long fun (int n, String str, int[] arr);
其方法签名: (ILjava/long/String;[I)J
括号里的内容分成三部分,之间没有空格。括号外面是返回值类型签名
5. JNI操作java对象
JNI提供了java和C/C++方法互操作的机制。JNI方法接受的第二个参数是java对象:jobject,可以在JNI中操作这个jobject,进而操作java对象提供的变量和方法。
(1)访问java对象
JNI提供的类和对象操作函数常用的有 FindClass 和 GetObjectClass 。在C和C++中分别有不同的实现。
在C++中函数原型:
jclass FindClass(const char* name);//查找类信息
jclass GetObjectClass(jobject obj);//返回对象的类
在C中函数原型:
jclass (*FindClass)(JNIEnv*, const char*);
jclass (*GetObjectClass)(JNIEnv*, jobject);
jclass (*GetObjectClass)(JNIEnv*, jobject);
下面是LOG系统中,操作java对象的方式:
jclass clazz = env->FindClass("android/util/Log");
通过给FindClass传入要查找类的全限定类名(以“/”分割路径)即可,之后方法返回一个jclass对象,这样就可以操作这个类的方法和变量了。
(2)操作成员变量和方法
对于已经得到的类的引用,JNI用名字和类型签名来识别方法和域(变量)。通过这种方式操作对象上的域和方法可分为两步。
以log系统为例,如下:
首先,通过FindClass方法找到android/util/Log的类信息clazz; 然后,以clazz为参数调用GetStaticFieldID(clazz, "DEBUG", "I"),其中DEBUG是要访问java域的名字,I是该java域的类型签名。GetStaticFieldID函数返回一个jfieldID, 代表java 成员变量。然后将该jfieldID传给GetStaticIntField方法,得到java层的成员变量DEBUG的值,即3。
在Log.java的源码中有定义 public static final int DEBUG = 3;
JNI调用java层的方法与此类似,流程是: FindClass->GetMethodID返回(jmethodID)->Call<Type>Method
下面是JNI提供的操作域和方法的函数:
(3)全局引用,弱全局引用和局部引用
java对象的生命周期由虚拟机管理,虚拟机内部维护一个对象的引用计数,如果一个对象的引用计数为0, 这个对象将被垃圾回收器回收并释放内存。如果java对象中使用了Native方法,那么对对象的生命周期的影响有什么?
例如如下:
上述代码中使用自定义的jobject对象来保存传进来的jobject对象。如上,两种方法都有问题,因为虚拟机无法跟踪该对象的引用计数。如果jobject已经被虚拟机回收了,那么自定义的clazz_ref1和clazz_ref2都引用了一个野指针,会有很大的问题。
JNIEnv提供了解决方案:局部变量,全局变量和弱全局变量
三种方法的定义:
对于弱全局引用,其所指对象可能被回收了,JNI提供了isSameObject函数来判断其是否被回收了。
如此,就可在JNI中处理和保存对象了:
android对局部引用和全局引用都有一定限制。引用超过一定数量,或者使用不当,就会引起内存不足或内存泄露的问题。
6. JNI异常处理
JNI函数在执行过程中会出现异常,其异常处理机制和java和C++都不同,JNI提供两种检查异常的方法:
方法1: 检查上一次JNI函数调用的返回值是否为NULL
方法2: 通过调用JNI函数ExceptionOccurred()来判断是否发生异常
检查到异常后,处理方法有两种:
处理1: Native方法可选择立即处理返回,这样异常就会在调用该Native方法的java代码中抛出。所以在java代码中必须有捕获相应异常的嗲吗,否则程序直接退出。
处理2: Native方法可以调用ExceptionClear()来清理异常,然后执行自己的异常处理代码。
JNI提供的检查和处理异常的函数:
异常出现后,Native相关代码必须先检查清除异常,然后才能进行其他的JNI函数调用。当有异常未清除时,只有以下JNI异常处理函数可被安全地调用: ExceptionOccurred(), ExceptinoDescribe(), ExceptionClear().
具体使用如下:
第三章 android启动过程的底层实现
android支持多种启动模式,主要有正常模式(normal mode),安全模式(safe mode), 恢复模式(recovery mode), 工厂模式(factory mode), 快速启动模式(fastboot mode)等。
3.1 Android 正常启动模式流程
正式启动流程大体如下:
(1)系统加电,执行bootloader。 bootloader 负责初始化软件运行所需的最小硬件环境,最后加载内核到内存中。
(2)内核加载到内存后,将首先进入内核引导阶段,在引导阶段最后,调用 start_kernel 进入内核启动阶段。 start_kernel 最终启动用户空间的 init 程序。
(3)init 程序负责解析 init.rc 配置文件,开启系统守护进程。两个最重要的守护进程是 zygote 和 ServiceManager。前者是android启动的第一个Dalvik虚拟机,它将负责启动java世界的进程;后者是Binder 通信的基础。
(4)zygote虚拟机启动子进程system_server,在system_server 中开启了 android核心系统服务并将核心系统服务添加到ServiceManager,最后系统进入systemReady状态。
(5)在
systemReady 状态下,ActivityManagerService 与 zygote 中的 Socket 通信,通过 zygote 启动Home应用,进入系统桌面。
步骤(1)中的bootloader 依赖于硬件体系结构,对应不同厂家的独特bootloader程序。步骤(2)中与linux相关。
3.2 Kernel启动过程
android Kernel启动过程与标准linux Kernel的启动过程基本一致,都是对 start_kernel 函数的调用和执行。本节将分析 android正常启动流程的第二步:kernel启动过程。
本节涉及的源码文件如下:
》kernel/arch/