Android 取得成功的关键因素之一就是它丰富的框架集。
没有这些框架,Android 可能会和其他一些嵌入式 Linux 发布版本一样混得很差。
通过提供各种框架,Android 让应用可以很方便地创建进程,允许开发者使用高级的 Java 语言而不是底层的 C/C++语言进行编程。各种框架的不断增加也在进一步强化这一过程,因为有大量的用于进行图形、音频和硬件访问的API可供开发者使用。
使用 Java 的包命名规则后,Android 的框架会根据它们各自不同的功能被分割在各自不同的命名空间(namespace)中。位于命名空间 android.*
中的包是可以供开发者使用的,而位于com.android.*
中的包则是仅供系统内部使用的。
Android 也支持大多数位于命名空间java*
中的标准Java 运行时包。
Android 使用的所有框架都是被打包在设备的/system/framework 目录下的数个Java格式的*.jar
文件中的,而在 L版中则是被预编译进 boot.jar 文件中的。
尽管 AOSP 是开源的,但直接从JAR文件中找出相关的包也非常容易-只要调用 dexdump (或者dextra 工具)直接分析JAR
文件中的 classes.dex 文件就行了。
Android 对 Linux 另一个值得注意的扩展就是引入了 Dalvik 虚拟机。
Dalvik 虚拟机是让Android 能够在256M 内存就已经算“很大”了的移动设备中正常工作的关键因素。Dalvik 并不是第一种试图能够运行在移动设备上的虚拟机,Sun 公司的 Microsystems 曾经被认为有望能够压过 Java 2移动版(J2ME)一头,但实际上收效甚微。
Dalvik 主要是由 Dan Bornstein 发明的一一他2008 年在谷歌I/0大会上的演讲被认为是了解Dalvik 虚拟机设计的一份很重要的参考资料。
虚拟机的名字 (Dalvik) 是为了纪念冰岛北部的一个小渔村。
Dalvik 虚拟机尽管看上去和 Java 是等价的,但实际上并不是一个Java 虚拟机。虽然偏离得并不是很远,但它运行的是一种完全不同形式的字节码(这种字节码叫作 DEX,也就是“Dalvik EXecutable”的缩写),而且相对于 Sun/Oracle 设计的JVM,它在执行效率和共享内存方面做了更多的优化。这些优化使得它在受到严格限制的移动平台上占尽优势一这一点也正是 Java(特别是 J2ME)没法在有限的实现之外进一步获得增长的原因。
Android 选择Apache 的 Harmony 文件的一个子集作为它的核心类(core class)的基础。之所以选择 Harmony 是因为它是免费的(在 Apache 许可协议下)(原来是 Sun 的,现在是 Oracle的)JVM 的开源克隆体。Oracle 于 2010 年将谷歌告上了法庭,理由就是谷歌从未正确地获得Java 类库的使用授权,这场旷日持久的官司甚至直到 2015 年初还没有了结。
Dalvik 虚拟机正在被ART (Android 运行时,Android RunTime)逐步取代。但是这并不意味着 Dalvik 正在走向消亡。因为 Dalvik 只有在即时编译 (JIT,Just-In-Time compilation)方面的部分会被取代,而它使用的(DEX)文件格式作为至关重要的体系结构概念,仍是非常有生命力的。
Android 应用是运行在虚拟机里的,但有时,通常是在需要访问硬件或其他设备 (或芯片集)特有的功能时,它还是需要执行虚拟机之外的代码的。
所以 Dalvik 允许应用通过 JavaNative Interface(JNI)使用原生代码库(ELF 共享库)中的代码。
从某种程度上说,Android 对JNI是又爱又恨。厂商们无疑更青睐于“纯”Dalvik 的应用。
因为所有的代码都是运行在虚拟机里的,所以不会受到虚拟机/操作系统是运行在什么体系结构的处理器上的影响。
在这种情况下,Android 应用可以在无须任何修改的情况下运行在任意一个平台上,无论是 Intel、ARM、MIPS 还是其他什么处理器上。
但是另一方面,虚拟机环境也并非没有限制(特别是在开发者非常关心图形处理问题时) 和缺陷(特别是它很容易被反编译)。因此在应用中使用JNI以优化性能或对抗逆向工程的情况也是屡见不鲜的。
有鉴于此,谷歌也提供了使开发者能够生成原生库 (及二进制可执行程序)的原生代码开发包(NDK,NativeDevelopment Kit)。
我们打开APK文件,常常可以看到对应的动态库.so 或者静态库.a:
并非所有的应用中都使用了 JNI,但在那些使用了 JNI的应用中,我们也可以很方便地在安装包 (.apk
文件)中找到JNI库,因为它们是被放在一个单独的文件夹 “/lb/architecture中的。
从 Linux 的角度讲,所有的可执行文件都是 ELF 二进制可执行文件。
Android 中的关键系统组件都是用 C/C++编写,并被编译成原生的二进制可执行文件的。而用户的应用则是编译成Dalvik 字节码的,但字节码是运行在 Dalvik 虚拟机的上下文环境中的(或者在 ART 中,是在运行之前被编译成原生代码的)。
而 Dalvik 虚拟机本身也是一个ELF格式的二进制可执行文件。因此,尽管大多数开发者大可以心安理得地忘掉“二进制可执行文件”这么一回事,但这些二进制可执行文件仍在Android 中扮演着重要的角色。
在Android中,二进制可执行文件通常都被放在/system/bin和/system/xbin这两个目录中(当然,还有一些重要的二进制可执行文件是放在/sbin 目录中的)。由于它们本身就是 AOSP 的一部分,所以无论是在哪种设备中,大多数的二进制可执行文件都是一样的。但是厂商或者芯片集的制造商往设备里添加一些额外的二进制可执行文件的情况也不少见。
我们可以随时执行 ps 命令,查看通过加载二进制可执行文件而运行起来的进程的列表。
因为ELF是个标准的文件格式,所以可以使用任何一种Linux ELF 文件解析工具(比如readelf、objdump或者其他 binutils 工具集中的工具) 分析 Android 的二进制可执行文件。
Android NDK在“toolchains/”
目录中也提供了完整的工具集(使用交叉编译技术编译的,这些工具就能运行在移动设备上了)。
在Android NDK中,binutils是一组二进制工具,用于处理和操作二进制文件,包括可执行文件和目标文件。以下是一些常见的Android NDK中的binutils工具:
这些工具通过NDK中的bin目录提供,路径类似于:<NDK_DIR>/toolchains/<ARCH>/prebuilt/<HOST_PLATFORM>/bin/
,其中<NDK_DIR>
是NDK的根目录,<ARCH>
是目标架构(例如arm、x86等),<HOST_PLATFORM>
是宿主平台(例如windows、linux等)。
我们可以使用这些binutils工具来执行各种任务,如编译和链接原生代码,调试和分析二进制文件等。
与Linux 发行版中使用GNU的LibC(GLibC作为它们的核心运行时(也就是著名的 libc.so)不同,Android选用了它自己的C运行时库一Bionic。
尽管谷歌宣称选择 Bionic 的理由主要是因为它的简洁性,但实际上合法性的考虑也占了很重要的位置。如果在 Android 中使用了使用GPL(GNU public license ,GNU 公共授权协议)授权的 GLibC,那么根据GPL,Android也就必须开源(这有点像 Linux 内核中使用GPL的情形),而这又是谷歌所要极力规避的。而Bionic 尽管也是开源的,但它混合使用了 BSD 授权协议(BSD 授权协议对使用相关软件的第三方软件的限制更少些) 和谷歌自己的授权协议。
Bionic是Android操作系统使用的C标准库。它是为了满足Android平台的需求而设计的,因此与传统的C标准库(如glibc)有一些区别。
Bionic库提供了一组API和功能,用于支持Android操作系统的核心功能,包括进程管理、内存管理、线程创建、文件操作等。Bionic库还对某些标准C库函数进行了优化和改进,以提高性能和适应Android系统的特定需求。
与传统的C标准库相比,Bionic库在以下方面有所不同:
Bionic库是Android NDK的一部分,开发者可以使用NDK来编译和构建原生代码,并使用Bionic库提供的功能和API来开发Android应用程序的核心部分。
《最强Android书:架构大剖析》