Physical Address:
ChongQing,China.
WebSite:
最近由于项目需要,需要将第三方库移植到Android系统。在此之前我并没有开源库的移植经验,抱着一边工作一边学习的态度总算是完成了移植工作,而这篇文章将对近期的移植工作做一个总结,以供后期查备。
所谓移植,也就是让第三方库在Android平台上运行并能够被正常调用。由于很多第三方库都不是为Android系统开发的,为了能够让其正常运行,就涉及到交叉编译。
交叉编译简单来讲就是在A平台上编译出可以在B平台上运行的程序。对于Andoid开发者而言,就是在Windows/Linux/Mac系统上编译出可以在Android上运行的程序。
我们都知道C/C++的编译需要经过四个步骤:预处理、编译、汇编、链接。这些都需要编译器的支持,不同的平台、不同标准的支持程度(如C++11/C++17/C++21),编译器也需要做对应的选择。我们在交叉编译时第一点需要明确的就是编译工具链(编译器)的种类。
目前主流的编译器种类如下:
GCC:GNU Compiler,是目前应用最为广泛,最为出名的编译器,不仅支持C/C++(GCC/G++),也支持其他的语言。对各类编程语言的新标准、新特性支持力度是比较大的。
Clang/LLVM:这是另外一套著名的编译器组合,其中Clang作为前端,LLVM作为后端,可以为编译提供更好的组件化支持,如编译优化等,相比GCC支持也更多的语言。
Intel C++ Compiler:是Intel为Windows开发平台所开发维护的编译器,也是Microsoft Visual Studio所使用的编译器。
Keil C++ Compiler:ARM 出品的用于在Windows上开发arm嵌入式平台的编译器,很多单片机/MCU/STM32等开发则是使用该编译器。
除此之外还有一些编译器,他们的应用场景各自不同。而Android系统的编译器选择也经历过两个阶段,在Android7.1.1版本之前,使用GCC编译器;在这之后则使用Clang/LLVM作为编译器。故而在移植时我们就需要考虑到这一点,需要选择合适的编译器。
编译环境包括编译器的选择,但除了编译器本身,还有一些基础的依赖库,以及系统所支持的一些标准库。由于我们移植的第三方库中可能会存在一些系统调用,或者是一些标准库依赖,这些都是与平台绑定的。如Linux平台的Glibc,Android平台的Bionic-c等。这些基础的依赖库是由编译环境所提供的。
在Android系统中,为开发者提供了统一的开发环境——NDK,也就是Native Developement Kit,在NDK中为我们提供了Android平台上可用的基础依赖库。我们移植C/C++库到Android系统中时,就需要基于NDK的编译环境去做配置。
NDK目前提供了Windows(64bit)、Linux(64bit)与Mac三个系统的安装包,我们可以根据自己的实际需要来选择合适的安装包,可通过该Link进行下载,同时也可以通过该链接查看每个NDK版本之间的差异与改动。
NDK版本会对应到Android版本以及Android API版本。以下为一个简单的对照表:
代号 | 版本 | NDK版本 | API 级别 |
Android12L | 12.1 | NDK r25c | API 级别 32 |
Android12 | 12 | NDK r25c | API 级别 31 |
Android11 | 11 | NDK r24b | API 级别 30 |
Android10 | 10 | NDK r23b | API 级别 29 |
Pie | 9 | NDK r22b | API 级别 28 |
Oreo | 8.1.0 | NDK r21e | API 级别 27 |
Oreo | 8.0.0 | NDK r21e | API 级别 26 |
Nougat | 7.1 | NDK r20b | API 级别 25 |
Nougat | 7.0 | NDK r20b | API 级别 24 |
Marshmallow | 6.0 | NDK r20b | API 级别 23 |
更完整的列表可以参考here。
这里需要说明的是,NDK的版本会对应Android版本是指NDK的版本会随着Android版本的发布而一同发布,并不是指NDK只支持对应的Android版本,事实上同一个NDK版本是支持不同API级别的。而这些不同API级别可能跨越不同的Android版本。
移植时我们需要关注NDK版本,因为不同的NDK版本支持的API级别是不一样的,而这些API级别同样会影响基础依赖库的实现。如何查看API级别,我们可以随便从Android系统中找出某个系统库使用file命令进行查看:
trout_x86:/vendor/lib #
trout_x86:/vendor/lib # file android.hardware.automotive.evs@1.1.so
android.hardware.automotive.evs@1.1.so: ELF shared object, 32-bit LSB 386, for Android 32, BuildID=6efcc183a452ae5e68e3cb8d78eb83a6, stripped
trout_x86:/vendor/lib #
trout_x86:/vendor/lib #
如图,我们系统的API级别为32,需要使用支持API级别的NDK进行编译。
相信有很大部分人认为编译系统与编译环境两者是同一类东西,但实际上两者是不一样的。同一套编译系统可以支持不同的编译环境。那么什么是编译系统呢,在我自己的理解里,编译系统是帮我们配置编译环境的。因为在编译时,我们可能需要某些条件判断,可能需要设定某些编译参数,可能需要解析某些编译配置。而这些,都是由编译系统所支持的。
这里举两个例子,比如Android系统编译时的soong编译系统,在编译时识别的是Android.mk或者Android.bp;而许多开源系统中比较常用的CMake编译系统,会根据顶层目录的CMakeList.txt以及*.cmake来构建。
编译系统很明显的一点就是,一般都提供了一层warp配置文件,来控制我们编译时的许多参数。但从实质上来讲,大部分的编译系统其实底层都依赖于GNU Make,编译系统就是帮我们生成各种makefile和编译参数。
在移植C/C++库至Android系统中时,编译环境是必须基于NDK的。但是编译系统我们是可以选择的。在Google官方文档中,目前支持的编译系统包括三类:
1.基于make的ndk-build编译系统
2.CMake编译系统
3.基于Autotools/Autoconf/libtool的编译系统
下面将会针对这三种编译系统进行展开,讲解如何基于这三种编译系统去进行交叉编译/移植。
使用NDK-Build时,环境配置是非常简单的,基础环境需要GNU Make 4的版本。在NDK环境中已经包含。使用NDK-Build时,其内部原理其实就是用基础的make命令进行编译,不过需要通过-f参数指定基础的makefile,如下所示:
$GNUMAKE -f <ndk>/build/core/build-local.mk
具体使用方法:
首先我们需要位于我们待移植源码的顶层目录,使用如下命令:
${NDK_DIR}/ndk-build
一些支持的选项如下:
clean:清除编译缓存,作用相当于make clean
-B:强制执行完整的重新构建。
NDK_LOG:显示NDK Build过程中的日志消息。
NDK_HOST_32BIT:配置是否始终使用32位模式下的工具链。
NDK_APPLICATION_MK:配置所使用的Application.mk。
在正在开始编译之前,我们需要编写相应的Application.mk与Android.mk,这对于许多开发者人员是比较麻烦的。一般我们移植的第三方库都有成熟的编译系统支持,能够支持NDK的编译工具链,此时我们不推荐使用ndk-build这种方式去完成编译。但如果你移植的第三方库只有源代码,并没有成熟的编译系统做支持,那使用ndk-build是比较合理的。
CMake起源于2000年,截至到今天已经走过了23个年头。作为当今开源世界广泛采用的编译系统,其效率与灵活性都得到了开发人员们的认可。
NDK为Cmake编译系统提供了官方支持的工具链文件,CMake通过该工具链文件可以完成基础的设定,关于工具链文件的信息,可以参考Cmake官方网站的说明。
这里先看一下使用Cmake编译系统的编译流程,我们进入到工程项目的顶层目录之后,会看到一个名为CMakeLists.txt的文件,这是CMake编译系统编译配置的入口,在其内部可能会include其他的.cmake编译配置。此时我们一般只需要经过以下几个步骤即可完成编译:
mkdir build && cd build //创建build文件夹,将用于存储编译过程中的中间产物
cmake ..
make -j4 //进行编译
make install //将编译好的内容安装到指定位置
使Cmake调用NDK提供的工具链该如何使用命令行参数呢,在上述步骤不变的情况下,我们只需要在第二步通过CMake参数来完成设定,示例如下:
cmake \
-DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=$ABI \
-DANDROID_PLATFORM=android-$MINSDKVERSION \
$OTHER_ARGS
以下将对各个参数做出释义:
-DCMAKE_TOOLCHAIN_FILE:用于指定NDK提供的工具链文件,其中$NDK代表NDK的安装路径,我们可以手动设置该环节变量,也可以直接使用绝对路径作为替代,该参数是必须的。
-DANDROID_ABI:用于指定与Android运行环境所匹配的CPU/指令集组合。Android设备可以运行在多种CPU架构上,而不同的CPU支持不同的指令集。我们在交叉编译时就需要明确目标运行环境的CPU与指令集。目前支持的ABI选项包括:armeabi-v7a
、arm64-v8a
、x86
、x86_64
.
-DANDROID_PLATFORM:用于指定底层依赖库所支持的最低API级别,该参数也可以通过其他别名来进行设定,如android-$API_LEVEL
,$API_LEVEL
等。通常一个NDK版本会支持多个API级别。如何查看每个NDK版本所支持的API级别呢,我们可以下载NDK后解压进行查看,如下图所示:
FranzKafkaYu@:/opt/FranzKafkaYu/ndk/android-ndk-r21e/platforms$ ls -la
total 60
drwxr-xr-x 15 FranzKafkaYu FranzKafkaYu 4096 Jan 11 2021 .
drwxr-xr-x 13 FranzKafkaYu FranzKafkaYu 4096 Mar 29 17:02 ..
drwxr-xr-x 4 FranzKafkaYu FranzKafkaYu 4096 Jan 11 2021 android-16
drwxr-xr-x 4 FranzKafkaYu FranzKafkaYu 4096 Jan 11 2021 android-17
drwxr-xr-x 4 FranzKafkaYu FranzKafkaYu 4096 Jan 11 2021 android-18
drwxr-xr-x 4 FranzKafkaYu FranzKafkaYu 4096 Jan 11 2021 android-19
drwxr-xr-x 6 FranzKafkaYu FranzKafkaYu 4096 Jan 11 2021 android-21
drwxr-xr-x 6 FranzKafkaYu FranzKafkaYu 4096 Jan 11 2021 android-22
drwxr-xr-x 6 FranzKafkaYu FranzKafkaYu 4096 Jan 11 2021 android-23
drwxr-xr-x 6 FranzKafkaYu FranzKafkaYu 4096 Jan 11 2021 android-24
drwxr-xr-x 6 FranzKafkaYu FranzKafkaYu 4096 Jan 11 2021 android-26
drwxr-xr-x 6 FranzKafkaYu FranzKafkaYu 4096 Jan 11 2021 android-27
drwxr-xr-x 6 FranzKafkaYu FranzKafkaYu 4096 Jan 11 2021 android-28
drwxr-xr-x 6 FranzKafkaYu FranzKafkaYu 4096 Jan 11 2021 android-29
drwxr-xr-x 6 FranzKafkaYu FranzKafkaYu 4096 Jan 11 2021 android-30
FranzKafkaYu@:/opt/FranzKafkaYu/ndk/android-ndk-r21e/platforms$
这意味着android-ndk-r21e版本所支持的Android API最低为16,最高为30。如果我们不进行指定,则默认会使用该NDK所支持的最低API级别。为了保证顺利移植,我们务必指定与我们运行环境所匹配的API版本。否则可能会在编译阶段报出很多错误。这里我们可以打开一个NDK提供的头文件(此处以android-ndk-r21e/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/include/pthread.h为例)进行查看:
#if __ANDROID_API__ >= 28
int pthread_attr_getinheritsched(const pthread_attr_t* __attr, int* __flag) __INTRODUCED_IN(28);
#endif /* __ANDROID_API__ >= 28 */
int pthread_attr_getschedparam(const pthread_attr_t* __attr, struct sched_param* __param);
int pthread_attr_getschedpolicy(const pthread_attr_t* __attr, int* __policy);
int pthread_attr_getscope(const pthread_attr_t* __attr, int* __scope);
int pthread_attr_getstack(const pthread_attr_t* __attr, void** __addr, size_t* __size);
int pthread_attr_getstacksize(const pthread_attr_t* __attr, size_t* __size);
int pthread_attr_init(pthread_attr_t* __attr);
int pthread_attr_setdetachstate(pthread_attr_t* __attr, int __state);
int pthread_attr_setguardsize(pthread_attr_t* __attr, size_t __size);
#if __ANDROID_API__ >= 28
int pthread_attr_setinheritsched(pthread_attr_t* __attr, int __flag) __INTRODUCED_IN(28);
#endif /* __ANDROID_API__ >= 28 */
int pthread_attr_setschedparam(pthread_attr_t* __attr, const struct sched_param* __param);
int pthread_attr_setschedpolicy(pthread_attr_t* __attr, int __policy);
int pthread_attr_setscope(pthread_attr_t* __attr, int __scope);
int pthread_attr_setstack(pthread_attr_t* __attr, void* __addr, size_t __size);
int pthread_attr_setstacksize(pthread_attr_t* __addr, size_t __size);
int pthread_condattr_destroy(pthread_condattr_t* __attr);
#if __ANDROID_API__ >= 21
int pthread_condattr_getclock(const pthread_condattr_t* __attr, clockid_t* __clock) __INTRODUCED_IN(21);
#endif /* __ANDROID_API__ >= 21 */
int pthread_condattr_getpshared(const pthread_condattr_t* __attr, int* __shared);
int pthread_condattr_init(pthread_condattr_t* __attr);
我们可以看到很多函数 定义都由__AND ROID_API__宏进行控制,如果API版本不匹配,可能会导致无法找到对应的函数定义。
除了上述比较重要的参数外,还支持如下参数,此处给出一个命令参数示例:
arguments :
-DCMAKE_FIND_ROOT_PATH=${HOME}/Dev/ello-jni/app/bug/prefab/armeabi-v7a/prefab
-DCMAKE_BUILD_TYPE=Debug
-DCMAKE_TOOLCHAIN_FILE=${HOME}/Android/Sdk/ndk/build/cmake/android.toolchain.cmake
-DANDROID_ABI=armeabi-v7a
-DANDROID_NDK=${HOME}/Android/Sdk/ndk/22.1.7171670
-DANDROID_PLATFORM=android-23
-DCMAKE_ANDROID_ARCH_ABI=armeabi-v7a
-DCMAKE_ANDROID_NDK=${HOME}/Android/Sdk/ndk/22.1.7171670
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
-DANDROID_STL=c++_shared
-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=${HOME}/Dev/github-/universalDebug/obj/armeabi-v7a
-DCMAKE_RUNTIME_OUTPUT_DIRECTORY=${HOME}/Dev/dk-samples/Debug/obj/armeabi-v7a
-DCMAKE_MAKE_PROGRAM=${HOME}/Android/Sdk/cmake/3.10.2.4988404/bin/ninja
-DCMAKE_SYSTEM_NAME=Android
-DCMAKE_SYSTEM_VERSION=23
Autotools是另外一套编译系统,相比于CMake历史更为悠久,其致力于GNU标准下的编译配置,是很多Linux发行版和许多基础库所采用的编译系统。
基于Autotools的编译系统在编译时一般遵循如下步骤:
1./configure //顶层目录下执行
2.make //编译
3.make install //安装
如果是采用Autotools编译系统的开源库如何移植呢。Google当然也想到了这一点,为我们提供了相应的工具来支持交叉编译。
在NDK环境中,提供了一个make_standalone_toolchain.py的Python脚本,在旧的NDK版本中是名为make-standalone-toolchain.sh的Shell脚本,我们可以通过这个脚本生成并安装自定义工具链。在Autotools的编译系统中,我们需要通过该脚本完成自定义工具链的设定,举例如下:
# Create an arm64 API 26 libc++ toolchain.
$NDK/build/tools/make_standalone_toolchain.py \
--arch arm64 \
--api 26 \
--install-dir=my-toolchain
我们可以通过该脚本的help参数来获取它所支持的参数信息:
–arch:与Cmake中类似,用于定义ABI。需要与我们Android系统的运行环境相匹配,目前支持arm\arm64\x86\x86_64选项。
–api:用于设定Android API级别,该参数同样很重要。
–install-dir:用于设定自定义工具链的安装路径。
–stl:用于指定所使用的STL标准库。
在安装完自定义工具链后,我们还需要手动设置编译环境,如下所示:
# Add the standalone toolchain to the search path.
export PATH=$PATH:/path-to/my-toolchain/bin
# Tell configure what tools to use.
target_host=aarch64-linux-android
export AR=$target_host-ar
export AS=$target_host-clang
export CC=$target_host-clang
export CXX=$target_host-clang++
export LD=$target_host-ld
export STRIP=$target_host-strip
# Tell configure what flags Android requires.
export CFLAGS="-fPIE -fPIC"
export LDFLAGS="-pie"
export LDFLAGS="-llog"
之后我们在原来的步骤基础上稍作变化即可进行编译:
1./configure --host=$target_host//顶层目录下执行
2.make //编译
3.make install //安装
以上即是开源库移植Android系统的常见方法,编译完成后我们可以将编译产物放入到Android运行环境中,如果是二进制binary,需要push到/system/bin目录下,如是so共享库,64位需要push到/system/lib64,而32位则push到/system/lib目录。
我们可以通过ldd命令查看其相应的依赖是否正常,如下为我将Google protobuf移植到Android系统后的示例:
trout_x86:/ # ldd /system/lib/libprotobuf.so
linux-gate.so.1 => [vdso] (0xf30a0000)
liblog.so => /system/lib/liblog.so (0xf2a09000)
libz.so => /system/lib/libz.so (0xf29c9000)
libc++_shared.so => /system/lib/libc++_shared.so (0xf2a40000)
libm.so => /apex/com.android.runtime/lib/bionic/libm.so (0xf2b47000)
libc.so => /apex/com.android.runtime/lib/bionic/libc.so (0xf2383000)
libdl.so => /apex/com.android.runtime/lib/bionic/libdl.so (0xf2b9d000)
libc++.so => /system/lib/libc++.so (0xf22c4000)
trout_x86:/ #
如果还有其他问题,如undefined reference这类问题导致无法正常运行时,我们可以通过nm、readelf、objdump等命令来进行排查。