一、前言
当我们把C/C++代码写好了之后,如何编译成动态库(Java端代码调用)?这就需要我们编写一个构建文档(Android.mk/Application.mk) NDK交叉编译器根据构建文档编译出我们需要的、Java端可以调用的动态库;
Android NDK --初始android NDK介绍了android NDK组件:
ARM、x86和MIPS交叉编译器:我们可以使我们的项目支持一或多个交叉编译器
构建系统:是开发人员能够用很短的构建文档来描述原生的Androud应用程序;
Java原生接口头文件:比如 jni.h
C库
MATH库
POSIX线程
最小的C++库
ZLib压缩库
动态链接库
Android日志库
Android像素缓冲区库
Android原生应用APIs
OpenGL ES 3D图形库
OpenSL ES 原生音频库
OpenMAX AL 最小支持
上面详细描述了NDK的全部知识点,后面的博客中我都会一一介绍。
我们如果把NDK组件的全部内容都掌握了,NDK开发就变得很简单了;现在好多同学都会NDK开发但大部分人都“知其然不知其所以然”。就是说只知道配置简单的 android.mk,复杂的项目就蒙了。甚至不知道android.mk是用来干什么的!你现在知道了吗?
二、知识点概述
我写这篇博客的目的是:
1.理解交叉编译,如何在android上进行交叉编译
2.理解构建系统是干什么的?如何使用?
这边博客重点是构建系统的使用,关于交叉编译我们现在只需要理解是什么意思就可以啦!关于交叉编译的实践(像第三方库的裁剪及使用:ffmpeg、libx264、png…),我会在后面的博客中会介绍的。
三、知识点详解
(1)交叉编译
在了解交叉编译之前,为了更好的理解交叉编译,我们先来理解下什么时本机编译;
本机编译: 平常做的编译叫本地编译,也就是在当前平台编译,编译得到的程序也是在本地执行。------来自百度百科
它的意思就是在Linux平台编译源代码,也是在Linux平台执行就是本机编译;但我们如果在Linux平台编译一个在android平台执行的代码,那就是交叉编译;
现在应该明白了什么是交叉编译了;下面在介绍下交叉编译的定义:
交叉编译: 简单地说,就是在一个平台上生成另一个平台上的可执行代码。同一个体系结构可以运行不同的操作系统;同样,同一个操作系统也可以在不同的体系结构上运行。------百度百科
Android平台交叉编译
android NDK组件中包含了ARM、x86和MIPS交叉编译器,我们通过该交叉编译器可以编译第三方库。第三方库都是需要进行交叉编译的,后面的交叉编译实践中我会详细讲解如何通过交叉编译器编译出我们需要的第三方库。像ffmpge/libx264/liblamb/libpng…
(2)构建系统
1、android NDK根目录下的结构
- ndk-build : 启动Android的构建系统,该Shell脚本是Android NDK构建系统的起始点,一般在项目中仅仅执行这一个命令就可以编译出对应的动态链接库了。
- ndk-gdb:该shell脚本允许用GUN调试器调试Native代码;
- ndk-stack:该shell脚本可以帮助分析原生组件崩溃时堆栈追踪;
- build:包含Android构建系统的所有模块;最重要的在build/core目录中;后面我们会分析它们在构建系统中的作用;
- platforms:包含了支持不同Android目标版本的头文件和库文件;Android构建系统会根据具体的Android版本自动引用这些文档;
- samples: Android NDK示例应用程序;
- sources:可供开发人员导入到现有的Android NDK项目的一些共享模块;
- toochains:该目录包含目前NDK所支持的不同平台下的交叉编译器——ARM、x86、MIPS,其中比较常见的是ARM和x86。构建系统会根据具体的配置选择不同的交叉编译器。
Android NDK构建系统是由多种GUN Makefile片段构成的。主要目的是使开发人员能够用很短的构建文档来描述原生代码;还处理了包括指定工具链、平台、CPU等很多细节;该构建系统包括基于渲染构建过程的不同类型NDK项目所需要的必要片段;这些构建片段可以在Android NDK根目录下的build/core子目录中找到。开发人员不会直接接触到这些文件。除了这些片段,Andorid NDK构建系统还要依赖另外两个文件:Android.mk 和 Application.mk , 这两个文件由开发人员提供,下面让我们来具体学习下这个两个文件。
2、Android.mk
Android.mk是一个向Android NDK构建系统描述NDK项目的GUN Makefile片段。它是NDK项目的必备组件。在我们的建立的项目中打开它,内容如下:
#把Native代码构建成libhelloworld.so动态库文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := helloworld
LOCAL_SRC_FILES := helloworld.cpp
include $(BUILD_SHARED_LIBRARY)
为了更好的理解它,我们逐行分析。
-
第一行 “#”是注释,GNU Makefile工具不会对它进行处理。
-
第二行: LOCAL_PATH := $(call my-dir)
用来定义LOCAL_PATH变量的,根据Android 构建系统的要求,Android.mk必须以LOCAL_PATH变量开头。Android 构建系统根据LOCAL_PATH来定位Android.mk文件的路径。 -
第三行:include $(CLEAR_VARS)
Android 构建系统清除除了LOCAL_PATH以外的LOCAL_< name >变量,例如LOCAL_MODULE与LOCAL_SRC_FILES等。这样做是因为Android构建系统在单次执行中解析多个构建文件和模块定义,而LOCAL_< name >是全局变量。清除它们可以避免不必要的冲突。我们的Android.mk也可以这样定义。
#把Native代码构建成libhelloworld.so动态库文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := helloworld
LOCAL_SRC_FILES := helloworld.cpp
include $(BUILD_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := helloworld2
LOCAL_SRC_FILES := helloworld2.cpp
include $(BUILD_SHARED_LIBRARY)
上面产生了两个模块,每一个模块生成前都要调用 include $(CLEAR_VARS)。
- 第四行:LOCAL_MODULE := helloworld
LOCAL_MODULE变量用来给这些模块设定一个唯一的名称。我们在Java端加载Native动态库的时候需要用到它。
static {
System.loadLibrary("helloworld");
}
- 第五行:LOCAL_SRC_FILES := helloworld.cpp
LOCAL_SRC_FILES变量用来指定该helloworld模块所包含的源文件。我们项目中只有一个源文件 helloworld.cpp;多个文件空格分开;
LOCAL_SRC_FILES := helloworld.cpp helloworld2.cpp helloworld.cpp helloworld3.cpp
- 第六行:include $(BUILD_SHARED_LIBRARY)
把 helloworld 模块构建成共享库文件 libhelloworld.so。
3、模块构建类型
一个模块(LOCAL_MODULE := helloworld)不仅仅可以构建成共享库,还可以构建成静态库、Prebuilt库、独立的可执行文件。特别是当我们的项目很复杂,包含很多个模块时它们之间会交错构建。比如我们通过Native实现了一个播放器,Native代码包括了Jni、Demux、Decode、Render等。当模块之间有关联时、当我们需要构建的动态库轻量时就需要我们灵活运用每个模块的构建类型。
下面介绍下一些我们常用的一个模块构建类型:
构建共享库
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := helloworld
LOCAL_SRC_FILES := helloworld.cpp
include $(BUILD_SHARED_LIBRARY)
执行如下命令:生成 libhelloworld.so共享库。
构建多个共享库
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := helloworld1
LOCAL_SRC_FILES := helloworld1.cpp
include $(BUILD_SHARED_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := helloworld2
LOCAL_SRC_FILES := helloworld2.cpp
include $(BUILD_SHARED_LIBRARY)
jni目录中执行ndk-build 后生成libhelloworld1.so、libhelloworld2.so两个共享库
构建静态库
Android NDK构建系统也支持静态库。实际上Android 应用程序并不直接使用静态库并且应用程序中也不包含静态库。静态库可以用来构建共享库。例如:我们在使用第三方代码时,不用直接使用源代码,而是先将第三方源代码编译成静态库,再把静态库构建成共享库。
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello_static
LOCAL_SRC_FILES := hello_static.cpp
include $(BUILD_STATIC_LIBRARY)
include $(CLEAR_VARS)
LOCAL_MODULE := helloworld
LOCAL_SRC_FILES := helloworld.cpp
LOCAL_STATIC_LIBRARIES := hello_static
include $(BUILD_SHARED_LIBRARY)
jni目录中执行ndk-build 后生成libhelloworld.so共享库,hello_static静态库并入了libhelloworld.so共享库。
用共享库共享通用模块
静态库可以保证源代码模块化;但是,当静态库与共享库相连时,它就变成了共享库的一部分。在多个共享库的情况下,多个共享库与同一个静态库连接时,需要将通用模块的多个副本与不同共享库重复相连,这样就增加了应用程序的大小。在这种情况下,不用构建静态库,而是将通用模块作为共享库建立起来,而动态连接依赖模块以便消除重复的副本。
LOCAL_PATH := $(call my-dir)
#
# 第三方AVI库
#
include $(CLEAR_VARS)
LOCAL_MODULE := avilib
LOCAL_SRC_FILES := avilib.c platform_posix.c
include $(BUILD_SHARED_LIBRARY)
#
# 原生模块 1
#
include $(CLEAR_VARS)
LOCAL_MODULE := module1
LOCAL_SRC_FILES := module1.c
LOCAL_SHARED_LIBRARIES := avilib
include $(BUILD_SHARED_LIBRARY)
#
# 原生模块 2
#
include $(CLEAR_VARS)
LOCAL_MODULE := module2
LOCAL_SRC_FILES := module2.c
LOCAL_SHARED_LIBRARIES := avilib
include $(BUILD_SHARED_LIBRARY)
Prebuilt库
使用共享模块要求有共享模块的源代码,Android NDK构建系统简单地把这些源文件包含在NDK项目中并每次构建它们。自R5版本以后,Android NDK也提供对Prebuilt库的支持。在下面的情况下,Prebuilt库是非常有用的:
-
想在不发布源代码的情况下将你的模块发布给他人。
-
想使用共享模块的预建版来加速构建过程。
尽管已经被编译了,但预建模块仍需要一个Android.mk构建文档,如程序:
LOCAL_PATH := $(call my-dir)
#
# 第三方预构建AVI库
#
include $(CLEAR_VARS)
LOCAL_MODULE := avilib
LOCAL_SRC_FILES := libavilib.so
include $(PREBUILT_SHARED_LIBRARY)
LOCAL_SRC_FILES变量指向的不是源文件,而是实际Prebuilt库相对于LOCAL_PATH的位置
我们在进行FFmpeg开发的时候,经常先通过交叉编译把FFmpeg编译成静态库或动态库。最后通过Prebuilt库并入到我们的项目中。
构建独立的可执行文件
构建独立的可执行文件,在常规的app开发中用的不多,它使用的环境经常在已经Root之后的手机上运行。
其他常用的构建变量
- LOCAL_C_INCLUDES: 用来引用头文件;
- LOCAL_CFLAGS: 编译C或者Cpp的编译标志,在实际编译的时候会发送给编译器。
- LOCAL_LDLIBS: 链接标志的可选列表,当对目标文件进行链接以生成输出文件的时候,将这些标志带给连接器。它主要用于传送要进行动态连接的系统库列表。例如:要与android NDK日志库链接:
LOCAL_LDLIBS := -llog
其他构建系统函数宏
本节概括了Android NDK构建系统支持的其他函数宏。
- all-subdir-makefiles:返回当前目录的所有子目录下的Android.mk构建文件列表。例如,调用以下命令可以将子目录下的所有Android.mk文件包含在构建过程中:
include $(call all-subdir-makefiles)
-
this-makefile:返回当前Android.mk构建文件的路径。
-
parent-makefile:返回包含当前构建文件的父Android.mk构建文件的路径。
-
grand-parent-makefile:和parent-makefile一样但用于祖父目录。
定义新变量
开发人员可以定义其他变量来简化他们的构建文件。以LOCAL_和NDK_前缀开头的名称预留给Android NDK构建系统使用。建议开发人员定义的变量以MY_开头,如程序:
...
MY_SRC_FILES := avilib.c platform_posix.c
LOCAL_SRC_FILES := $(addprefix avilib/, $(MY_SRC_FILES))
...
条件操作
Android.mk构建文件也可以包含关于这些变量的条件操作,例如:在每个体系结构中包含一个不同的源文件集,如程序:
...
ifeq ($(TARGET_ARCH),arm)
LOCAL_SRC_FILES + = armonly.c
else
LOCAL_SRC_FILES + = generic.c
endif
...
3、Application.mk
Applicaton.mk也是一个GUN Makefile片段。它的目的是描述应用程序需要那些模块。
- APP_CPPFLAGS: 该变量列出了一些编译器标志,在编译任何模块的C++源文件时这些标志都会被传送给编译器。
- APP_CFLAGS:该变量列出了一些编译器标志,在编译任何模块的C++源文件时这些标志都会被传送给编译器。
- APP_ABI: 默认情况下,Android NDK构建系统为armeabi ABI生成二进制文件。可以用该变量改变上述行为,并为其他ABI生成二进制文件,例如:
APP_ABI := mips
另外,可以设置多个ABI:
APP_ABI := armeabi mips
为所有支持的ABI生成二进制文件:
APP_ABI := all
- APP_STL:默认情况下,Android NDK构建系统使用最小STL运行库,也被称为system库。可以用该变量选择不同的STL实现。
APP_STL :=stlport_shared
使用NDK-Build脚本
如前所述,可以通过执行ndk-build脚本启动Android NDK构建系统。该脚本用一组参数使维护和控制构建过程更容易。
- 默认情况下,ndk-build脚本应该在主项目目录中执行。-C参数可以用于指定命令行中国NDK项目的位置,这样以来ndk-build脚本可以从任意的位置开始。
ndk-build -C /hone/zhangjunling/jni/
- 如果源文件没被修改,Android NDK构建系统不会重建目标。可以用-B执行nkd-build脚本强制重建所有源代码。
ndk-build -B
- 为了清理生成的二进制文件和目标文件,可以在。命令行执行ndk-build clean命令。Android NDK构建系统会删除生成的二进制文件。
ndk-build clean
- Andorid NDK 构建系统依赖与GNU Make 工具对模块进行构建。默认情况下,GNU Make工具一次执行一句构建命令,等这一句完成以后在执行下一句。如果在命令行使用-j参数。GNU Make就可以并行执行构建命令。另外,也可以通过指定该参数之后的数字来指定并行执行的命令总数。
ndk-build -j 3
参考文献:
- Android C++高级编程
- 网络博客