前言

最近自己一直在做有关 Android 系统源码底层的开发,就经常接触到 Android NDKAOSP(Android Open Source Project) Build System 这两个东西,但是由于他们两者都可以将 C/C++ 代码编译成可执行文件或者动态链接库,导致我经常将这两者弄混淆了。所以,痛定思痛,不想再被这种似四而非的感觉折磨了,今天就抽空写下这篇文章来捋清楚两者之间关系。

Android NDK

Android NDK 是什么?

先引用一段来自 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 的用途:

Android NDK 应用实例

例如,在图像处理中我们常用的 OpenCV 库就是使用 C++ 编写的,如果我们想在我们使用 Java 开发的 Android APP 中使用 OpenCV 库中的一些处理函数,那么该怎么办呢? 当然,你可以直接去找基于 Java 实现的 OpenCV 的 jar 包,然后去调用对应的函数,但是这种Java 实现版本的 OpenCV 在处理的效率上肯定不及 C++ 实现版本的 OpenCV(尤其是在做图形处理方面)。 所以,另外一种方法就是通过 Android NDK 工具将 OpenCV 的代码编译成指定硬件平台的库文件,然后在 Android APP 进程中通过 JNI 的方式来使用 OpenCV 中提供的处理函数,实现自己想要的某种功能。

Android NDK 编译系统

Android NDK 编译系统其实本质上就是一系列的交叉编译工具链,而 NDK 中所使用的编译脚本 ndk-build 就是根据编译配置文件 Android.mkApplication.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
  • build:该目录下包含了 ndk-build 编译脚本中所使用到的各种小的脚本文件,例如我们在 Android.mk 中常见的将源码编译成共享链接库文件的语句: include $(BUILD_SHARED_LIBRARY)中的 BUILD_SHARED_LIBRARY 对应的就是一个编译脚本文件: <ndk-home>/build/core/build-shared-library.mk
  • ndk-build:NDK 的核心,NDK 编译所使用的最主要的编译脚本。
  • ndk-gdb:调试 NDK 编译出来的库的工具。
  • ndk-stack:对 Android 系统运行时共享链接库崩溃时产生的 tombstone 文件进行解析的工具。它在调试异常崩溃 Bug 时定为到具体出错的源码位置非常有帮助,具体使用方法详见我的这篇博客:Android NDK Tombstone/Crash 分析
  • platforms:这个文件夹里保存的是各个 Android 系统版本下不同硬件平台对应的 NDK 提供的系统共享链接库文件,例如:liblog.so, libdl.so, libc.so等等
  • source:这个目录下主要包含的就是 NDK 所中的 C++ STL 库的源码,有 gnu_stl 的实现,也有 llvm-stl的实现。
  • toolchains:这个目录也是非常关键的一个目录,它里面包含了不同目标硬件平台下面的交叉编译工具,可以将 C/C++ 库编译成可运行在 arm,x86,x86-64,mips等不同硬件平台上面。

这里就不对 Android.mk 编译配置文件的编写方法展开说明了,有关内容可以参见下面这篇文章:Mastering Android NDK Build System - Part 1: Techniques with ndk-buildAndroid.mk

Android NDK 中系统提供的共享链接库

有时我们的自己编写的源码中除了实现某种特定的功能之外,可能还会在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 Build System

AOSPAndroid Open Source Project 的简称,接下来用我就用它等价地表示 Android 系统源码。

AOSP Build System 是什么?

AOSP Build System 是用来编译 Android 系统,Android SDK 以及相关文档的一套框架。该编译系统主要由 Make 文件(注意:这里的 Make 文件不是 Makefile 文件,而是 Android 编译系统自己构架的一套编译配置文件,通常以*.mk 为文件后缀),Shell 脚本以及 Python 脚本组成,其中最主要的是 Make 文件。

在 Android Build System 中编译所使用到的 Make 文件主要分为三类:

  • 第一类是 Build 系统核心文件,此类文件定义了整个 Build 系统的框架,而其他所有 Make 文件都是在这个框架的基础上编写出来的。
  • 第二类是针对某个产品(一个产品可能是某个型号的手机或者平板电脑)的 Make 文件,这些文件通常位于 device 目录下,该目录下又以公司名以及产品名分为两级目录。
  • 第三类是针对某个模块的 Make 文件。整个 Android 系统中,包含了大量的模块,每个模块都有一个专门的 Make 文件,这类文件的名称统一为 Android.mk,该文件中定义了如何编译当前模块。Build 系统会在整个源码树中扫描名称为“Android.mk”的文件并根据其中的内容执行模块的编译。

AOSP Build System 所依赖的编译工具

Android 系统从下到上主要分为下面5层,而每一层所使用的编程语言如下:

  • Kernel:Android 定制化过的 Linux Kernel,使用的当然是 C 语言了。
  • HAL:这一层是用户态驱动层,它主要功能是和下层 Kernel 中的硬件驱动程序进行交互,这一层主要使用的语言是 C/C++
  • Frameworks native:这一层是 Android 系统核心组件的实现位置,在这一层中主要通过 C++ 语言来实现。
  • Frameworks java:这一层其实是对 Frameworks native 层套上一层 Java 的外壳,封装成 Android SDK 提供给上层的 APP 开发者进行调用,这一层主要使用了 Java 语言进行实现。
  • APP:这一层主要面向的是 Android APP 开发人员,并且 Android 提供的 SDK 是基于 Java 语言的,所以 APP 的代码实现也是 Java 语言。

既然 Android 系统源码中包含了 3 种编程语言,那么在 AOSP Build System 中肯定也使用了许多编译工具来进行编译(Android 官方推荐使用 Ubuntu 14.04 来对 Android 源码进行编译,所以这里就以 Ubuntu 系统中所使用的编译工具为例):

  • 编译 C/C++ 代码:使用 Ubuntu 14.04 中自带的 gcc 编译器即可。
  • 编译 Java 代码:在 Ubuntu 14.04 中推荐使用 OpenJDK-1.7 来进行编译。
  • make 工具:前面提到 AOSP Build System 是基于 make 工具,所以这里也使用 Ubuntu 14.04 中自带的 make 工具即可。

AOSP Build System 编译系统共享链接库的方法

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 NDKAOSP 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 系统的日志系统就挂了。

系统共享链接库的支持

  • Android NDK 中支持用户自己编写的 C/C++ 代码链接调用一部分系统共享链接库文件,例如 liblog.so, libOpenSLES.so, libGLESv2.so 等库文件,有关 Andorid NDK 支持那些系统库文件,详见 NDK 的官方文档:Android NDK Native APIs
  • AOSP Build System 就好比是系统共享链接库的“妈”,既然这些库文件都由它编译出来的,那么 AOSP 中任意一个模块肯定都可以使用全部的系统共享链接库文件。