区分基础概念:JNI 与 NDK
- JNI(Java Native Interface)是一种 Java 语言特性
用于 Java 程序与 C、C++ 库间的互相调用。
- NDK(Native Development Kit)是 Google 提供的使用 C/C++ 编写 Android 程序的开发工具包
它使用 JNI 实现 Java 程序调用 C/C++ 本地代码,允许 C/C++ 本地代码访问 Android API,不只是用来开发或移植 C/C++ 库,也可以是 C/C++ 程序。
引用自 Android NDK | Android NDK | Android Developers
Android NDK
The Android NDK is a toolset that lets you implement parts of your app in native code, using languages such as C and C++. For certain types of apps, this can help you reuse code libraries written in those languages.
NDK 提供一系列稳定的 C/C++ API,头文件在 sysroot/usr/include
下,主要包括 C 标准库、C++ 标准库、jni、math、pthread、zlib、OpenGL、Android 相关的库,
NDK 支持的 API 也会随着需求的增加而日趋完善。
移植使用 GNU Autotools 的项目
NDK 提供了创建独立工具链的工具,对于移植使用 GNU Autotools 的项目到 Android 平台很有帮助,省去写 Android.mk
。
创建独立工具链
/opt/android-ndk/build/tools/make_standalone_toolchain.py \ --arch arm64 --api 28 --stl=libc++ --install-dir /opt/android-toolchain
环境变量配置
export PATH=$PATH:/opt/android-toolchain/bin export CC=aarch64-linux-android-clang export CXX=aarch64-linux-android-clang++ export LD=aarch64-linux-android-ld export AR=aarch64-linux-android-ar export STRIP=aarch64-linux-android-strip export RANLIB=aarch64-linux-android-ranlib export AS=aarch64-linux-android-clang export CFLAGS="-fPIE -fPIC" export CXXFLAGS="-fPIE -fPIC" export LDFLAGS="-pie" export SYSROOT=/opt/android-toolchain/sysroot export CROSS_COMPILE_HOST=aarch64-linux-android
交叉编译
./configure --host=${CROSS_COMPILE_HOST} --prefix=/opt/local make make install
移植 Nginx 遇到的问题
编译出错
cstddef:43:25: fatal error: stddef.h: No such file or directory
看起来是 C++ 编译器找不到 C 头文件,是已知问题,在 Standalone 工具链中使用
gcc
就会出现,见stddef.h: No such file or directory · Issue #215 · android-ndk/ndk
改为使用
clang
就好了,以后的 NDK 将彻底移除对gcc
的支持。交叉编译 OpenSSL
网上有大量的移植文档,基本上是基于 OpenSSL Android - OpenSSLWiki ,最大的问题是使用的 NDK 和 OpenSSL 版本都比较旧,最后主要参考
couchbaselabs/couchbase-lite-libcrypto: Pre-built OpenSSL libcrypto static libraries
移植成功。OpenSSL 的交叉编译需要完整的 NDK 包,最好新开一个 Shell 来编译 OpenSSL,避免为独立工具链设置的环境变量影响到 OpenSSL 的编译。
编译过程中会报错
crtbegin_so.o: No such file: No such file or directory
,将它从工具链中拷到当前目录即可,crtend_so.o
、crtbegin_dynamic.o
、crtend_android.o
也进行相同处理,参考 gcc - crtbegin_so.o missing for android toolchain (custom build) - Stack Overflow# Download wget https://codeload.github.com/openssl/openssl/zip/OpenSSL_1_1_0-stable -O openssl-OpenSSL_1_1_0-stable.zip unzip openssl-OpenSSL_1_1_0-stable.zip wget https://raw.githubusercontent.com/couchbaselabs/couchbase-lite-libcrypto/master/build-android-setenv.sh -O openssl.setenv sed -i -e 's/^_ANDROID_NDK=/#_ANDROID_NDK=/g' openssl.setenv # Config export ANDROID_NDK_ROOT=/opt/android-ndk export _ANDROID_TARGET_SELECT=arch-arm64-v8a export _ANDROID_NDK="android-ndk" export ANDROID_EABI_PREFIX=aarch64-linux-android export _ANDROID_EABI="${ANDROID_EABI_PREFIX}-4.9" export _ANDROID_ARCH=arch-arm64 export _ANDROID_API="android-28" source openssl.setenv # Build cd openssl-OpenSSL_1_1_0-stable ./Configure dist ./Configure no-ssl2 no-ssl3 no-comp no-hw no-engine --openssldir=/opt/local/ --prefix=/opt/local/ linux-generic64 -DB_ENDIAN -B$ANDROID_DEV \ -I${ANDROID_NDK_ROOT}/sysroot/usr/include -I${ANDROID_NDK_ROOT}/sysroot/usr/include/${ANDROID_EABI_PREFIX} \ -fPIE -pie -L${ANDROID_NDK_ROOT}/platforms/${_ANDROID_API}/${_ANDROID_ARCH}/usr/lib ln -s ${SYSROOT}/usr/lib/crtbegin_so.o ln -s ${SYSROOT}/usr/lib/crtend_so.o ln -s ${SYSROOT}/usr/lib/crtbegin_dynamic.o ln -s ${SYSROOT}/usr/lib/crtend_android.o make -j6 make install
交叉编译出的配置检测程序无法直接运行
Nginx 没有使用 GNU Autotools,而是有自已的 Configure 脚本,遇到的第一个问题就是 Nginx 是通过使用编译器编译一个测试程序来获取配置信息,交叉编译出来的程序肯定是无法在编译机上运行的,一个可靠的办法就是修改 Configure 脚本,改为上传到目标设备上执行。
下面是我写好的一个远程执行脚本
execute
#!/bin/bash if [ $# != 2 ] || ([ $1 != "-c" ] && [ $1 != "-f" ]) ; then echo "usage: $0 -<c|f> <cmd|file>" >> /dev/stderr exit 22 #Invalid argument fi LOG_FILE=/dev/null if [[ "$CROSS_COMPILE_HOST" = *"android"* ]]; then adb connect ${CROSS_COMPILE_DEVICE_IP} >>$LOG_FILE 2>&1 adb wait-for-device adb root >>$LOG_FILE 2>&1 adb connect ${CROSS_COMPILE_DEVICE_IP} >>$LOG_FILE 2>&1 adb wait-for-device adb remount >>$LOG_FILE 2>&1 adb connect ${CROSS_COMPILE_DEVICE_IP} >>$LOG_FILE 2>&1 adb wait-for-device if [ $1 == "-f" ]; then tempfile=`mktemp -u XXXXXXXX` tempdir=/tmp/cross-compile adb shell mkdir ${tempdir} >>$LOG_FILE 2>&1 adb push $2 ${tempdir}/${tempfile} >>$LOG_FILE 2>&1 adb shell "find ${tempdir} -ctime +1 -type f -exec rm {} \; >/dev/null 2>&1; cd ${tempdir} && chmod a+x $tempfile && ./$tempfile" else adb shell $2 fi else if [ $1 == "-f" ]; then tempfile=`mktemp -u XXXXXXXX` tempdir=/tmp/cross-compile ssh -o StrictHostKeyChecking=no root@${CROSS_COMPILE_DEVICE_IP} mkdir ${tempdir} >>$LOG_FILE 2>&1 scp -o StrictHostKeyChecking=no $2 root@${CROSS_COMPILE_DEVICE_IP}:${tempdir}/${tempfile} >>$LOG_FILE 2>&1 ssh -o StrictHostKeyChecking=no root@${CROSS_COMPILE_DEVICE_IP} "find ${tempdir} -mmin +1 -type f -exec rm {} \; >/dev/null 2>&1; cd ${tempdir} && chmod a+x $tempfile && ./$tempfile" else ssh -o StrictHostKeyChecking=no root@${CROSS_COMPILE_DEVICE_IP} $2 fi fi
修改
nginx-1.14.0
的配置脚本,将本地运行测试程序的地方改为远程执行sed -i -e 's/\/bin\/sh -c $NGX_AUTOTEST/timeout 10 \/build\/execute -f $NGX_AUTOTEST/g' `find auto -type f` sed -i -e 's/$NGX_AUTOTEST >\/dev\/null/timeout 10 \/build\/execute -f $NGX_AUTOTEST >\/dev\/null/g' `find auto -type f` sed -i -e 's/`$NGX_AUTOTEST`/`timeout 10 \/build\/execute -f $NGX_AUTOTEST`/g' `find auto -type f`
运行时需要预先设置环境变量
CROSS_COMPILE_DEVICE_IP
为目标设备 IP,对于嵌入式 Linux 设备,需要使用ssh-copy-id
命令设置为本机免密码 ssh 登录,对于 Android 设备需要预先 adb 授权通过。编译时找不到
crypt.h
crypt.h
属于glibc
,glibc
是 Linux 下的 C 标准库实现,除了实现ANSI C
标准库还有大量方便 Linux 开发的扩展功能。而 NDK 提供的 C 标准库并非glibc
而是 Bionic libc ,这导致移植 Nginx 时由于缺少crypt.h
头文件而编译不过。可以将
crypt
调用替换为DES_crypt
sed -i -e 's/#include <crypt.h>/#if (NGX_HAVE_CRYPT_H)\n#include <crypt.h>\n#endif\n#include <openssl\/des.h>/g' src/os/unix/ngx_linux_config.h sed -i -e 's/= crypt(/= DES_crypt(/g' src/os/unix/ngx_user.c
链接时找不到
glob
函数NDK 工具链是有提供
glob.h
头文件的,但是链接时却找不到glob
函数。网络上有很多单独提供 glob 下载的,但是都无法直接通过编译,因为里面有很多平台相关的特性。Undefined reference to glob and globfree in libc.so · Issue #718 · android-ndk/ndk 中说是已经在
r17b
版本解决,需安装android-ndk-r17-beta2
同时设置API Level
为28
即可。交叉编译 Nginx
可使用独立工具链来交叉编译 Nginx,参考前面环境变量配置部分先设置好工具链的环境变量。
CC_TEST_FLAGS="${CFLAGS} ${LDFLAGS}" ./configure --with-ld-opt="${LDFLAGS}" --prefix=/opt/local --crossbuild=`/build/execute -c 'uname -srm' | tr ' ' ':'` --user=root --group=root --with-select_module --with-poll_module --with-file-aio --with-http_ssl_module --without-mail_pop3_module --without-mail_imap_module --without-mail_smtp_module make make install
编译出的 Nginx 无法在低版本的 Android 上运行
之前为了使用最新的 NDK 工具链包含的函数
glob
,而将API Level
设置成28
,这就意味着编译出来的程序无法在低版本的 Android(<=8.1) 上运行,在 Android 7.1 上运行会报错/system/bin/nginx: No such file or directory
。将 NDK 最新工具链带的
so
也拷到设备上,运行程序前设置一下$LD_LIBRARY_PATH
优先使用最新的so
,Nginx 运行后直接卡死或者立即崩溃。添加
-static -Wl,--dynamic-linker=/system/bin/linker
链接选项静态编译,运行时会报错/system/bin/nginx: Accessing a corrupted shared library
的错误,需要使用/system/bin/linker64
。由于
-static
与-pie
是互相冲突的选项,静态编译的程序运行时会报错only position independent executables (PIE) are supported.
。编译在 Android 5 及以上版本运行的 Nginx
参考以下项目,通过使用 docker 方便交叉编译 nginx-1.14.0
tangxinfa/android-nginx: Cross compile nginx with android ndk
结论
一般的 C/C++ 库通常本身就会注重可移植性,不会生硬地依赖系统底层特性,使用 NDK
移植是可行的,即使是 ffmpeg
这种大型的库也可以成功移植到 Android。
而对于一些 Linux 下的程序,使用 NDK 直接移植会有很大的失败机率,因为他们可能使用了 NDK 不支持的特性(如 glibc
)。
NDK 一直在改进,以前阻碍我们移植到 Android 的问题很可能会在新版本中解决,遗憾的是编译出的程序无法运行在旧版本 Android 上。
参考
- Concepts | Android NDK | Android Developers
- Android NDK vs AOSP Build System
- Android NDK Native APIs | Android NDK | Android Developers
- Building an Android* Command-Line Application Using the NDK Build Tools | Intel® Software
- Android NDK开发扫盲及最新CMake的编译使用 - 简书
- 在命令行下用cmake交叉编译可在android中运行的so包 - CSDN博客
- cmake使用独立工具链交叉编译可在android中运行的so包 - CSDN博客
- Building Open Source libraries with Android NDK | Thoughts and Ideas In Warped Times
- 交叉编译带RTMP模块的Nginx到Android | Tom's Blog
- Android Static Linking vs Dynamic Linking against glibc - Stack Overflow
- android 5.0 lollipop - GCC: -static and -pie are incompatible for x86? - Stack Overflow