我通过阅读邓凡平前辈的《深入理解Android》,为了加深学习作此学习笔记。
虽然是邓老师2011著的书,但其中的安卓框架还是可以学习的。
另老师的csdn地址在: 阿拉神农的博客_CSDN博客-Android开发系列,深入理解Android,移动万态领域博主
tips:阅读该知识应具有C++的基本知识,因为本书的大部分内容都集中在了Native层。
本书是在分析 Android 源码的基础上展开的,而源码文件所在的路径一般都很长,例如,
文件 AndroidRuntime.cpp 的真实路径就是 framework/base/core/jni/AndroidRuntime.cpp
本书的编写顺序,是 6 、 5 、 4 、 7 、 8 、 9 、 10 、 2 、 3 、 1 章,但出于逻辑连贯性的 考虑,还是建议读者按本书的顺序阅读。其中,第 2 、 5 、 6 章分别讲述了 JNI 、 Android 常用类 ,以及 Binder 系统,这些都是基础知识,我们有必要完全掌握。其他部分的内容都是针对单个 模块的,例如 Zygote 、 Audio 、 Surface 、 MediaScanner 等,读者可各取所需,分别对其进行研究
该书是上册, 全书一共 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 。
第7 章 阐述了 Audio 系统中的三位重要成员 AudioTrack、AudioFlinger和AudioPolicyService 的工作原理 。本章拓展思考部分分析了 AudioFlinger 中 DuplicatingThread 的工作原理,并且和 读者一道探讨了单元测试、 ALSA 、 Desktop check 等问题。
第8 章 以 Surface系统 为主,分析了 Activity和Surface的关系、Surface和SurfaceFlinger的关系 以及SurfaceFlinger的工作原理 。本章的拓展思考部分分析了 Surface 系统中数据传输控制对象 的工作原理、有关 ViewRoot 的一些疑问,最后讲解了 LayerBuffer 的工作流程。这是全书中难 度较大的一章,建议大家反复阅读和思考,这样才能进一步深入理解 Surface 系统。
第9 章 分析了 Vold和Rild ,其中 Vold负责Android平台中外部存储设备的管理,而Rild负责与射频通信有关的工作 。本章的拓展思考部分介绍了嵌入式系统中与存储有关的知识,还探讨了 Rild 和 Phone 设计优化方面的问题。
第10 章 分析了多媒体系统中 MediaScanner的工作原理 。
第1章 阅读前的准备工作
1.1 系统架构
1.1.1 Android系统架构
该平台本身是基于 Linux 内核的
Linux 内核层:包含了 Linux 内核和一些驱动模块(比如 USB 驱动、 Camera 驱动、蓝牙驱动等)。目前 Android2.2 (代号为 Froyo )基于 Linux 内核 2.6 版本。
Libraries 层:这一层提供动态库(也叫共享库)、 Android 运行时库、 Dalvik 虚拟机等。从编程语言角度来说,这一层大部分都是用 C 或 C++ 写的,所以也可以简单地把它看成是 Native层。
Framework 层:这一层大部分用 Java 语言编写,它是 Android 平台上 Java 世界的基石。
Applications 层:与用户直接交互的就是这些应用程序,它们都是用 Java 开发的。
Android系统搭建出了一个java世界,他的运转依赖于另一个被Google极力隐藏的Native世界。
Java 虽具有与平台无关的特性,但 Java 和具体平台之间的隔离却是由 JNI 层来实现的。 Java是通过 JNI 层调用 Linux OS 中的系统调用来完成对应的功能的,例如创建一个文件或一个Socket 等。
除了 Java 世界外,还有一个核心的 Native 世界,它为整个系统高效和平稳地运行提供了强有力的支持。一般而言, Java 世界经由 JNI 层通过 IPC 方式与 Native 世界交互,而 Android 平台上最为神秘的 IPC 方法就是 Binder 了,第 6 章将详细分析 Binder 。除此之外, Socket 也是常用的IPC 方式。
1.1.2 本书的架构
本书所分析的模块也将遵循 Android 系统架构
该书籍所分析的各个模块除未涉及 Kernel 外,其他三层均有所涉及,它们分别 是:
Native层 包括 init、Audio系统(包括AudioTrack、AudioFlinger和AudioPolicyService)、Surface系统(包括Surface和SurfaceFlinger)、常用类(包括RefBase、sp、wp等)、Vold和 Rild 。
Java Framework层 包括 zygote、System_server以及Java中的常用类 (包括 Handler 和 Looper等)。
Java Application层 包括 MediaProvider和Phone 。
1.2 下载源码
下面将详细介绍如何下载 Android 的源码。
1. 设置软件源
将软件源地址设置成了http://mirror.bjtu.edu.cn/ubuntu。
1.2.1 下载 Android 源码
下面开始下载 Android 源码,工序比较简单,可一气呵成。
apt-get install git-core curl # 先下载这两个工具
mkdir-p/develop/download-froyo # 在根目录下建立 develop 和 download-froyo 两个目录
cd~/develop/download-froyo # 进入这个目录
curl http://Android.git.kernel.org/repo>./repo # 从源码网站下载 repo 脚本,该脚本是 Google
为了方便源码下载而提供的,通过该脚本可下载整套源码。
chmod a+x repo # 设置该脚本为可执行
./repo init-u git://Android.git.kernel.org/platform/manifest.git-b froyo # 初始化 git 库
./repo sync # 下载源码,大小约为 2GB ,如果网速快,估计也得要 2 个多小时。
注意 Kernel 的代码必须单独下载,下载方法如下:
git clone git : //android.git.kernel.org/kernel/common.git kernel
1.2.2 编译源码
1.部署JDK
Froyo 的编译依赖 JDK 1.5 ,所以首先要做的就是下载 JDK 1.5 。下载网址是
http://www.oracle.com/technetwork/java/javase/downloads/index-jdk5-jsp-142662.html 。
下载得 到的文件为 jdk-1_5_0_22-linux-i586.bin ,把它放到任意一个目录中,笔者将它放在了 /develop 中,然后在这个目录中执行如下命令:
./jdk-1_5_0_22-linux-i586.bin#执行这个文件
./jdk-1_5_0_22-linux-i586.bin#执行这个文件
这个命令的功能其实就是解压,解压后的结果在 /develop/jdk1.5.0_22 目录中。现在有了JDK ,再按照下面的步骤部署它即可:
( 1 )在~ /.bashrc 文件的末尾添加以下几句话:
export JAVA_HOME=/develop/jdk1.5.0_22#设置为刚才解压的目录 export JRE_HOME=JAVA_HOME/jre export CLASSPATH=$JAVA_HOME/lib:$JRE_HOME/lib:$CLASSPATH export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$PATH
(2 )重新登录系统,这样 JDK 资源就能被正确找到了。
2.编译源码
Android 的编译有自己的一套规则,主要利用的是 mk 文件,在此简单地介绍其编译工序:
- 进入源码目录(以笔者的开发环境为例,也就是cd/develop/download_froyo):
- 执行.build/envsetup.sh,这个脚本用来设置Android的编译环境。
- 执行choosecombo命令,这个命令用来选择编译目标(如目标硬件平台、eng还是user等)。一般而言,手机厂商会设置自己特有的编译选项。
- 执行完上面几个步骤后,就可以编译系统了。
Android平台提供了三个命令用于编译,它 们分别是 make、mmm和mm ,这三个命令的使用方法及其优劣如下:
make :不带任何参数,它用于编译整个系统,时间较长,笔者不推荐这种做法,除非读者 想编译整个系统。
make MediaProvider :下面几个例子都以编译 MediaProvider 为例。这种方式对应于单个模 块编译。它的优点是,会把该模块依赖的其他模块也一起编译。例如 make libmedia ,就会把 libmedia 依赖的库全部编译好。其缺点也很明显,它需要搜索整个源码来定位 MediaProvider 模 块所使用的 Android.mk 文件,并且还要判断该模块所依赖的其他模块是否有修改。整体编译 时间较长。
mmm packages/providers/MediaProvider :该命令将编译指定目录下的目标模块,而不编译 它所依赖的模块。所以,如果读者是初次编译,采用这种方式编译一个模块往往会报错。错误的原因是因为它依赖的模块没有被编译。
mm :这种方式需要先用 cd 命令进入 packages/providers/MediaProvider 目录,然后执行 mm 命 令。该命令会编译当前目录下的模块。它和 mmm 一样,只编译目标模块, mm 和 mmm 命令编译的速度都很快。
如果只知道目标模块的名称,则应使用make 模块名的方式来编译目标模块。例如,如果要 编译 libmedia ,则直接使用 make libmedia 即可。另外,初次编译时也要采用这种方法。
如果不知道目标模块的名称,但知道目标模块所处的目录,则可使用 mmm或mm 命令来编 译。当然,初次编译还必须使用 make 命令,以后的编译就可使用 mmm 或 mm 了,这样会节约 不少时间。
注意 一般的编译方式都使用增量编译,即只编译发生变化的目标文件,但有时则需重新编译所有目标文件,那么就可使用 make 命令的 -B 选项。例如 make-B 模块名,或者 mm-B 、mmm-B 。在 mm 和 mmm 内部,也是调用 make 命令的,而 make 的 -B选项将强制编译所有目标文件
3. 本书各模块的编译目标
本书各模块的编译目标如下所示,这里仅列出几个有代表性的模块:
目标模块 make命令 mmm命令
1.init make init mmm system/core/init
2.zygote make app_process mmm frameworks/base/cmds/app_process
3.system_server make services mmm frameworks/base/services/java
4.RefBase等 make libutils mmm frameworks/base/libs/utils
5.Looper等 make framework mmm frameworks/base
6.AudioTrack make libmedia mmm frameworks/base/media/libmedi
7.AudioFlinger make libaudioflinger mmm frameworks/base/libs/audioflinger
8.AudioPolicyService make libaudiopolicy mmm hardware/msm7 k/libaudio-qsd8k(示例)
假设make framework,那么编译完的结果则如图1-6所示
从上图可看出,make 命令编译了 framework-res.apk 和 framework.jar 两个模块。它们编译的结果在 out/target/product/generic/system/framework 下。利用 adb 命令把这两个文件 push 到手机的system/framework 目录即可替换旧的文件。
如果想测试这个新模块,则需要先杀掉所有使用该 模块的进程,进程重启后会重新加载模块,这时就能使用新的文件了。
例如,想测试刚修改的 libaudioflinger 模块,通过 adb 命令 push 上去后,要先杀掉 mediaserver 进程,因为 libaudioflinger 库目前只有该进程在使用。当 mediaserver 重启后,就会加载新 push 上来的libaudioflinger 库了。
注意 系统服务被杀掉后一般都会自动重启
1.3 工具介绍
1.3.1 Source Insight介绍
关于使用工具source insight的解释可以参考我另外一篇文档:
1.3.2 Busybox的使用
Busybox号称 Linux 平台上的 “ 瑞士军刀 ” ,它提供了很多常用的工具,例如 grep 和 find 等。这些工具在标准 Linux 上都有,但 Android 系统却去掉了其中的大多数工具。
这导致我们在调试 程序和研究 Android 系统时十分不便,所以我们需要在手机上安 Busybox 。
1. 下载 Busybox
我们可从下面这个网站中下载已编译好的Busybox :
http://www.busybox.net/downloas/binaries/1.18.4/
第2章 深入理解JNI
本章涉及源代码主要在以下路径:
- MediaScanner.java(framework/base/media/java/src/android/media/MediaScanner.java)
- android_media_MediaScanner.cpp(framework/base/media/jni/MediaScanner.cpp)
- android_media_MediaPlayer.cpp(framework/base/media/jni/android_media_MediaPlayer.cpp)
- AndroidRunTime.cpp(framework/base/core/jni/AndroidRunTime.cpp)
- JNIHelp.c(dalvik/libnativehelper/JNIHelp.c)
2.1 JNI概述
JNI 是 Java Native Interface 的缩写,中文译为 “ Java本地调用 ” 。
通俗地说, JNI 是一种技术,
通过这种技术可以做到以下两点:
- Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++编写的函数。
- Native程序中的函数可以调用Java层的函数,也就是说在C/C++程序中可以调用Java的函数。
JNI 技术的推出有以下几个方面的考虑:
承载Java 世界的虚拟机是用 Native语言 写的,而虚拟机又运行在具体的平台上,所以虚拟机本身 无法做到平台无关 。然而,有了 JNI 技术后就可以对 Java 层屏蔽不同操作系统平台(如Windows 和 Linux )之间的差异了(例如同样是打开一个文件, Windows 上的 API 使用 OpenFile函数,而 Linux 上的 API 是 open 函数)。这样,就能实现 Java 本身的平台无关特性。其实 Java 一直在使用 JNI 技术,只是我们平时较少用到罢了。
早在Java 语言诞生前,很多程序都是用 Native 语言写的,它们遍布在软件世界的各个角落。 Java 出世后,它受到了追捧,并迅速得到发展,但仍无法将软件世界彻底改朝换代,于是才有了折中的办法。既然已经有 Native 模块实现了相关功能,那么在 Java 中通过 JNI 技术直接使用它们就行了,免得落下重复制造轮子的坏名声。另外,在一些要求 效率和速度 的场合还是需要 Native 语言参与的。
在Android 平台上, JNI 就是一座将 Native 世界和 Java 世界间的天堑变成通途的桥。如下图 ,它展示了 Android 平台上 JNI 所处的位置:
在Android 平台上尽情使用Java 的程序员们不要忘了,如果没有 JNI 的支持,我们将寸步难行!
注意! 虽然 JNI 层的代码是用 Native 语言写的,但本书还是把与 JNI 相关的模块单独归类到 JNI 层了。
2.2 学习JNI的实例:MediaScanner
首先看第一个例子,是和MediaScanner相关的,如下图是:MediaScanner和它的JNI
Java世界对应的是 MediaScanner ,而这个 MediaScanner 类有一些函数需要由 Native 层来实现。
JNI层对应的是 libmedia_jni.so 。 media_jni 是 JNI 库的名字,其中,下划线前的 “media” 是Native 层库的名字,这里就是 libmedia 库。下划线后的 “jni” 表示它是一个 JNI 库。注意, JNI 库的名字可以随便取,不过 Android 平台基本上都采用 “ lib模块名_jni.so ” 的命名方式。
Native 层对应的是 libmedia.so ,这个库完成了实际的功能。
MediaScanner将通过 JNI 库 libmedia_jni.so 和 Native 层的 libmedia.so 交互。
从上面的分析中还可知道:JNI 层必须实现为动态库的形式,这样Java虚拟机才能加载它并调用它的函数
下面来看 MediaScanner 。
提示 :MediaScanner是Android平台中多媒体系统的重要组成部分,它的功能是扫描媒体文件,得到诸如歌曲时长、歌曲作者等媒体信息,并将它们存入到媒体数据库中,供其他应用程序使用。
2.3 Java层的MediaScanner分析
先来看 MediaScanner (简称 MS )的源码,这里将提取出与 JNI 有关的部分,其代码如下所示:
[-->MediaScanner.java] public class MediaScanner { static{static语句 /*①加载对应的JNI库,media_jni是JNI库的名字。在实际加载动态库的时候会将其拓展成 libmedia_jni.so,在Windows平台上则拓展为media_jni.dll */ System.loadLibrary("media_jni"); native_init();//调用native_init函数 } …… //非native函数 public void scanDirectories(String[]directories,String volumeName){ …… } //②声明一个native函数。native为Java的关键字,表示它将由JNI层完成。 private static native final void native_init(); …… private native void processFile(String path,String mimeType, MediaScannerClient client); …… }
上面代码中列出了两个比较重要的要点:一个是加载 JNI 库;另一个是 Java 的 native 函数。
2.3.1 加载JNI库
如果Java 要调用 native 函数,就必须通过一个 位于JNI层的动态库 来实现。顾名 思义,动态库就是运行时加载的库,那么在什么时候以及什么地方加载这个库呢?
这个问题没有标准答案,原则上是:在调用native 函数前,任何时候、任何地方加载都可以。通行的做法是在类的 static 语句中加载,调用 System.loadLibrary 方法就可以了。这一点在上面的代码中也见到了,我们以后就按这种方法编写代码即可。
另外,System.loadLibrary 函 数的参数是动态库的名字,即 media_jni 。系统会自动根据不同的平台拓展成真实的动态库文 件名,例如在 Linux 系统上会拓展成 libmedia_jni.so ,而在 Windows 平台上则会拓展成 media_jni.dll 。
2.3.2 Java的native函数和总结
从上面代码中可以发现,native_init 和 processFile 函数前都有 Java 的关键字 native ,它表示这两个函数将由 JNI 层来实现。
Java层的分析到此结束。 JNI 技术也很照顾 Java 程序员,只要完成下面两项工作就可以使用JNI 了:
- 加载对应的JNI库。
- 声明由关键字native修饰的函数。
2.4 JNI层MediaScanner的分析
MediaScanner (简称 “MS” )的 JNI 层代码在 android_media_MediaScanner.cpp 中,如下所示:
[-->android_media_MediaScanner.cpp] //①这个函数是native_init的JNI层实现。 static void android_media_MediaScanner_native_init(JNIEnv*env) { jclass clazz; clazz=env->FindClass("android/media/MediaScanner"); …… fields.context=env->GetFieldID(clazz,"mNativeContext","I"); …… return; } //这个函数是processFile的JNI层实现。 static void android_media_MediaScanner_processFile(JNIEnv*env,jobject thiz, jstring path,jstring mimeType,jobject client) { MediaScanner*mp=(MediaScanner*)env->GetIntField(thiz,fields.context); …… const char*pathStr=env->GetStringUTFChars(path,NULL); …… if(mimeType){ env->ReleaseStringUTFChars(mimeType,mimeTypeStr); } }
2.4.1 注册JNI函数
native_init 函数对应的 JNI 函数是 android_media_MediaScanner_native_init
native_init 函数位于 android.media 这个包中,它的全路径名应该是 android.media.MediaScanner.native_init ,而 JNI 层函数的名字是
android_media_MediaScanner_native_init 。因为 在Native语言中,符号“.”有着特殊的意义 ,所 以 JNI 层需要把 Java 函数名称(包括包名)中的 “.”换成“_” 。
上面其实讨论的是 JNI函数的注册 问题, “ 注册 ” 之意就是 将Java层的native函数和JNI层对应的实现函数关联起 来,有了这种关联,调用 Java 层的 native 函数时,就能顺利转到 JNI 层对应的函数执行了。
JNI 函数的注册方法实际上有两种
- 静态方法
- 动态注册