最近自己一直在做有关 Android 系统源码底层的开发,就经常接触到 Android NDK
和 AOSP(Android Open Source Project) Build System
这两个东西,但是由于他们两者都可以将 C/C++
代码编译成可执行文件或者动态链接库,导致我经常将这两者弄混淆了。所以,痛定思痛,不想再被这种似四而非的感觉折磨了,今天就抽空写下这篇文章来捋清楚两者之间关系。
先引用一段来自 Android NDK 官网 上的非常简洁的介绍吧:
The Android NDK is a toolset that lets you implement parts of your app using native-code languages such as C and C++. For certain types of apps, this can help you reuse code libraries written in those languages.
上面的介绍我觉得已经解释地非常清楚了,我再扩展地补充一下:Android NDK 本质上是一套 交叉编译工具集
,它可以将 C/C++ 源码编译成适用于不同硬件平台的 库文件
和 可执行文件
,而这些库文件和可执行文件可以被上层的基于 Java 语言编写的 APP 加载调用,从而实现了 C/C++ 源码在 APP 中的复用。
下面这幅图就非常简洁地体现了 NDK 的用途:
例如,在图像处理中我们常用的 OpenCV
库就是使用 C++ 编写的,如果我们想在我们使用 Java 开发的 Android APP 中使用 OpenCV 库中的一些处理函数,那么该怎么办呢?
当然,你可以直接去找基于 Java 实现的 OpenCV 的 jar 包,然后去调用对应的函数,但是这种Java 实现版本的 OpenCV 在处理的效率上肯定不及 C++ 实现版本的 OpenCV(尤其是在做图形处理方面)。
所以,另外一种方法就是通过 Android NDK 工具将 OpenCV 的代码编译成指定硬件平台的库文件,然后在 Android APP 进程中通过 JNI
的方式来使用 OpenCV 中提供的处理函数,实现自己想要的某种功能。
Android NDK 编译系统其实本质上就是一系列的 交叉编译工具链
,而 NDK 中所使用的编译脚本 ndk-build
就是根据编译配置文件 Android.mk
和 Application.mk
来调用这些交叉编译工具链中的工具编译生成指定 ABI 平台下目标链接库文件或者可执行文件。
这里我觉得还是有必要多费点文字对 Android NDK 包中的文件及目录的内容进行一个说明,以便大家对 NDK 有更加深一步的理解。NDK 包中的文件及目录结构如下所示:
woshijpf@woshijpf-OptiPlex-9020:~/Android/NDK/android-ndk-r12b$ tree -L 1 . |-- build |-- CHANGELOG.md |-- ndk-build |-- ndk-depends |-- ndk-gdb |-- ndk-stack |-- ndk-which |-- platforms |-- prebuilt |-- python-packages |-- shader-tools |-- source.properties |-- sources `-- toolchains 7 directories, 7 files
include $(BUILD_SHARED_LIBRARY)
中的 BUILD_SHARED_LIBRARY
对应的就是一个编译脚本文件: <ndk-home>/build/core/build-shared-library.mk
。 tombstone
文件进行解析的工具。它在调试异常崩溃 Bug 时定为到具体出错的源码位置非常有帮助,具体使用方法详见我的这篇博客: Android NDK Tombstone/Crash 分析 liblog.so, libdl.so, libc.so等等
。 C++ STL
库的源码,有 gnu_stl
的实现,也有 llvm-stl
的实现。 arm,x86,x86-64,mips
等不同硬件平台上面。 这里就不对 Android.mk
编译配置文件的编写方法展开说明了,有关内容可以参见下面这篇文章: Mastering Android NDK Build System - Part 1: Techniques with ndk-build 和 Android.mk
有时我们的自己编写的源码中除了实现某种特定的功能之外,可能还会在C/C++代码中使用到日志打印输出函数,而这个日志打印函数就位于 Android NDK 中 Android 系统提供的 liblog.so
库中。除了 liblog.so 库之外,NDK 还提供了下面这些系统共享链接库供我们自己的源码进行加载调用:
woshijpf@woshijpf-OptiPlex-9020:~/Android/NDK/android-ndk-r12b/platforms/android-22/arch-x86/usr/lib$ ls -al total 10996 drwxr-xr-x 2 woshijpf woshijpf 4096 Jun 15 2016 . drwxr-xr-x 4 woshijpf woshijpf 4096 Jun 15 2016 .. -rw-r--r-- 1 woshijpf woshijpf 2204 Jun 15 2016 crtbegin_dynamic.o -rw-r--r-- 1 woshijpf woshijpf 1992 Jun 15 2016 crtbegin_so.o -rw-r--r-- 1 woshijpf woshijpf 2204 Jun 15 2016 crtbegin_static.o -rw-r--r-- 1 woshijpf woshijpf 704 Jun 15 2016 crtend_android.o -rw-r--r-- 1 woshijpf woshijpf 648 Jun 15 2016 crtend_so.o -rwxr-xr-x 1 woshijpf woshijpf 10772 Jun 15 2016 libEGL.so -rwxr-xr-x 1 woshijpf woshijpf 34640 Jun 15 2016 libGLESv1_CM.so -rwxr-xr-x 1 woshijpf woshijpf 28428 Jun 15 2016 libGLESv2.so -rwxr-xr-x 1 woshijpf woshijpf 46592 Jun 15 2016 libGLESv3.so -rwxr-xr-x 1 woshijpf woshijpf 6752 Jun 15 2016 libOpenMAXAL.so -rwxr-xr-x 1 woshijpf woshijpf 7036 Jun 15 2016 libOpenSLES.so -rwxr-xr-x 1 woshijpf woshijpf 28876 Jun 15 2016 libandroid.so -rw-r--r-- 1 woshijpf woshijpf 8814206 Jun 15 2016 libc.a -rwxr-xr-x 1 woshijpf woshijpf 125464 Jun 15 2016 libc.so -rwxr-xr-x 1 woshijpf woshijpf 5400 Jun 15 2016 libdl.so -rwxr-xr-x 1 woshijpf woshijpf 5212 Jun 15 2016 libjnigraphics.so -rwxr-xr-x 1 woshijpf woshijpf 5436 Jun 15 2016 liblog.so -rw-r--r-- 1 woshijpf woshijpf 1333352 Jun 15 2016 libm.a -rwxr-xr-x 1 woshijpf woshijpf 26708 Jun 15 2016 libm.so -rwxr-xr-x 1 woshijpf woshijpf 18184 Jun 15 2016 libmediandk.so -rw-r--r-- 1 woshijpf woshijpf 105024 Jun 15 2016 libstdc++.a -rwxr-xr-x 1 woshijpf woshijpf 5536 Jun 15 2016 libstdc++.so -rw-r--r-- 1 woshijpf woshijpf 575800 Jun 15 2016 libz.a -rwxr-xr-x 1 woshijpf woshijpf 11768 Jun 15 2016 libz.so
那么这些系统提供的共享链接库是怎么被加载使用的呢?
例如,我现在写了一个 C++ 源文件 hello.cpp
,并且在这个源文件中调用了 __android_log_print()
函数打印日志,那么我就需要在对应的 Android.mk
文件中加入下面这条语句来显示地链接 /system/lib/liblog.so
库:
LOCAL_LDLIBS := -llog
那么这些系统共享链接库是如何编译出来的呢?
因为安全性和兼容性问题以及上层应用程序的需求,NDK 提供的只是 Android 系统中一小部分系统共享链接库,并且这些系统共享链接库都是在 Android 源码
编译时生成的,例如, liblog.so
库就是由 Android 源码中 /system/core/liblog/ 目录下的源码编译而来的。而在 $NDK/platforms/android-22/arch-arm/usr/include/android/log.h 头文件中声明的日志打印函数 __android_log_write()
的实际代码实现就在 Android 源码的 /system/core/liblog/logd_write.c
文件中。
Android NDK Native APIs
C++ Library Support AOSP
是 Android Open Source Project
的简称,接下来用我就用它等价地表示 Android 系统源码。
AOSP Build System
是用来编译 Android 系统,Android SDK 以及相关文档的一套框架。该编译系统主要由 Make 文件( 注意:这里的 Make 文件不是 Makefile 文件,而是 Android 编译系统自己构架的一套编译配置文件,通常以*.mk 为文件后缀 ),Shell 脚本以及 Python 脚本组成,其中最主要的是 Make 文件。
在 Android Build System 中编译所使用到的 Make
文件主要分为三类:
Android.mk
,该文件中定义了如何编译当前模块。Build 系统会在整个源码树中扫描名称为“Android.mk”的文件并根据其中的内容执行模块的编译。 Android 系统从下到上主要分为下面5层,而每一层所使用的编程语言如下:
Linux Kernel
,使用的当然是 C
语言了。 C/C++
。 C++
语言来实现。 Java
语言进行实现。 Java
语言。 既然 Android 系统源码中包含了 3 种编程语言,那么在 AOSP Build System
中肯定也使用了许多编译工具来进行编译( Android 官方推荐使用 Ubuntu 14.04 来对 Android 源码进行编译,所以这里就以 Ubuntu 系统中所使用的编译工具为例 ):
gcc
编译器即可。 OpenJDK-1.7
来进行编译。 make
工具即可。 在 AOSP Build System 是什么?
小节中,我提到了 Android 系统源码中也使用了 Android.mk
文件来将某个模块编译成库文件或者可执行文件。
例如,Android 系统源码中的 AudioFlinger
服务对应使用的是系统中的 libaudioflinge.so
共享链接库文件 ,该共享链接库的源码实现位于 frameworks/av/services/audioflinger
,在同一目录下面的 Android.mk
编译配置文件如下所示:
LOCAL_SRC_FILES:= / # 编译该模块所需要使用到的源文件 AudioFlinger.cpp / Threads.cpp / Tracks.cpp / Effects.cpp / AudioMixer.cpp.arm / PatchPanel.cpp LOCAL_SRC_FILES += StateQueue.cpp LOCAL_C_INCLUDES := / $(TOPDIR)frameworks/av/services/audiopolicy / $(call include-path-for, audio-effects) / $(call include-path-for, audio-utils) LOCAL_SHARED_LIBRARIES := / # 链接该模块所依赖的共享链接库文件 libaudioresampler / libaudioutils / libcommon_time_client / libcutils / libutils / liblog / libbinder / libmedia / libnbaio / libhardware / libhardware_legacy / libeffects / libpowermanager / libserviceutility LOCAL_STATIC_LIBRARIES := / # 链接该模块所依赖的静态链接库文件 libscheduling_policy / libcpustats / libmedia_helper LOCAL_MODULE:= libaudioflinger LOCAL_CFLAGS += -fvisibility=hidden #隐藏共享链接库中的符号,使之不被其他共享库所访问 include $(BUILD_SHARED_LIBRARY) # 编译成 libaudioflinger.so 库文件
在 Android 源码树的根目录下运行下面的命令来配置好 Android 源码编译的环境:
$ source build/envsetup.sh $lunch # 选择自己需要编译的 Android 系统版本
有了 Android 编译环境之后,只需要在将当前的工作目录切换到 frameworks/av/services/audioflinger 目录下来编译 AudioFlinger
模块
# 由于 libaudioflinger.so 所需依赖其他的系统共享链接库文件,所以需要先把整个 Android 源码生成这些共享链接库文件 $ mm # 读取当前工作目录下的 Android.mk 文件,编译 libaudioflinger.so 共享链接库文件
注意:虽然咋一看上去 Android 源码中某个模块的编译配置文件 Android.mk
和 NDK 中所用的编译配置文件 Android.mk
没有什么不同,但是其实还是有一些细微的区别的,尤其是在 使用共享链接库 方面。
例如,上面 Android 系统中源码编译出来的 libaudioflinger.so
库文件中链接 liblog.so
库文件使用的是 LOCAL_SHARED_LIBRARIES
编译变量:
LOCAL_SHARED_LIBRARIES := / # 链接该模块所依赖的共享链接库文件 ... liblog / ...
而在 NDK 编译自己使用 C/C++ 编写的模块时,如果要链接 liblog.so
库文件, Android.mk
文件中的写法则是:
LOCAL_LDLIBS := -llog
所以,我们可以看出来在 AOSP
中所有编译出来的系统链接库文件(不管是静态库文件还是共享链接库文件)对 AOSP
中各个模块都是可见和可以被链接使用的,而对于 NDK 来说它只能通过 LOCAL_LDLIBS
的变量来链接使用 Android 系统中提供的一小部分系统链接库文件。
有关 AOSP Build System 更加详细的介绍,可以参考下面的的文章:
理解 Android Build 系统
《Embedded Android》 Chapter 4 – The Build System
Android Build System Ultimate Guide
Establishing a Build Environment 前面我们对 Android NDK
和 AOSP Build System
做了比较详细的说明,所以在这一小节中就是对两者从下面几个方面进行一个差异对比:
Android NDK
它所面对的开发人员群体是 APP
开发人员,他们想使用 C/C++
代码来实现某种功能,然后在上层 APP 的 Java
代码中来通过 JNI 的方式来调用这些函数。例如,一些手机游戏 APP 的开发人员,为了使得游戏运行时画面更加流畅,他们就常常会把这些图像渲染这块耗时和性能要求较高的模块通过 C/C++
代码调用 OpenGLES API
函数来实现,然后通过 ndk-build
编译成共享库文件,然后被上层的 APP 中的 Java 代码加载调用。 AOSP Build System
面向的开发人员群体则是一些底层操作系统的开发人员,他们需要根据自己的需求和硬件平台的特性对 Android
源码进行一个定制修改,然后通过 AOSP Build System
重新编译得到自己想要的 Android 系统的镜像和库文件。 Android NDK
生成链接库或可执行文件的目的是为了执行实现上层 APP 层中的需要通过 C/C++
才能实现的某种功能,还是上面举过的例子,手机游戏 APP 开发人员需要通过 C/C++
才能实现一些性能要求很高的图像渲染操作。 AOSP Build System
中生成的链接库文件或可执行文件都是 Android 系统运行起来 必须依赖
的库文件,非常重要!!!例如,我们前面一直举的 liblog.so
就是由 AOSP Build System
编译出来的一个系统共享链接库文件,如果没有这个文件,那么 Android 系统的日志系统就挂了。