前言
本篇博客将详细介绍如何在 Windows 环境下使用 Android NDK 交叉编译 C 文件,生成静态和动态库,并在 Android 项目中使用这些库。
设置开发环境
由于编译需要在Linux环境中进行,如果你的电脑是Windows的,可以下载Ubuntu LTS 或者使用 VMware Workstation 15 Player 使用 Ubuntu 镜像都行,注意:不使用虚拟机的话使用 Windows 版本的NDK编译即可,使用虚拟机 因为完全是Linux环境,所以要下载Linux版本的NDK!我两种方式都试过了,个人感觉Ubuntu LTS 还是方便一些,接下来就开始一步一步介绍如何使用 Ubuntu LTS 进行交叉编译。
启用 WSL
WSL 允许在 Windows 上运行 Linux 环境,这对于使用 Android NDK 进行交叉编译至关重要。
- 启用 WSL 功能: 在 Windows 搜索栏中输入"启用或关闭 Windows 功能",然后勾选"适用于 Linux 的 Windows 子系统"选项。点击"确定"并重启电脑。
安装 Ubuntu
-
安装 Ubuntu: 打开 Microsoft Store,搜索并安装 Ubuntu 18.04 或更高版本。
-
初始化 Ubuntu: 安装完成后,启动 Ubuntu,设置用户名和密码。
安装 GCC 和 G++
- 更新包列表并安装构建工具: 打开 Ubuntu 终端,执行以下命令:
bash
sudo apt update
sudo apt install build-essential
编译器概述
编译器 | 概述 | 特点 |
---|---|---|
GCC(GNU Compiler Collection) | GNU项目开发的编译器集合,支持多种编程语言,包括C、C++、Fortran等。 | - 开源免费 :遵循GPL许可,广泛应用于开源项目。 - 跨平台支持 :支持Linux、Windows、macOS等多种操作系统和硬件架构。 - 强大的优化能力:提供多种优化选项,生成高效的目标代码。 |
G++ | GCC中的C++编译器驱动程序,专门用于编译C++源代码。 | - 支持C++标准 :兼容C++11、C++14、C++17、C++20等标准。 - 处理C++特性 :全面支持模板、面向对象编程、多态、异常处理等。 - 集成链接功能:简化编译过程,负责编译和链接阶段。 |
Clang | 基于LLVM项目的开源编译器前端,支持C、C++、Objective-C等语言。 | - 快速编译速度 :通常比GCC更快,提升开发效率。 - 友好的错误信息 :提供清晰、详细的编译错误和警告。 - 模块化设计 :易于与其他工具集成,支持插件和自定义扩展。 - 高可扩展性:适应不同的开发需求,支持静态分析和代码格式化工具。 |
编译C/C++文件的原理
阶段 | 作用 | 过程 | 输出 |
---|---|---|---|
预处理(Preprocessing) | 处理源代码中的预处理指令,如#include 、#define 等。 |
- 宏展开 :替换宏定义。 - 文件包含 :插入头文件内容。 - 条件编译:决定保留或忽略代码段。 | 生成扩展后的纯文本代码。 |
编译(Compilation) | 将预处理后的源代码转换为中间表示(如汇编代码)。 | - 词法分析 :分解源代码为词法单元。 - 语法分析 :组织成语法树(AST)。 - 语义分析 :类型检查、作用域解析等。 - 优化 :消除冗余、循环优化等。 - 代码生成:生成汇编代码或中间代码。 | 生成汇编代码文件(.s)或中间代码文件。 |
汇编(Assembly) | 将汇编代码转换为目标机器码,生成目标文件。 | - 指令翻译 :将汇编指令转换为机器码。 - 地址分配:为符号分配内存地址。 | 生成目标文件(.o或.obj)。 |
链接(Linking) | 将多个目标文件和库文件组合,生成最终的可执行文件或库文件。 | - 符号解析 :解决符号引用。 - 地址重定位 :分配最终内存地址。 - 库链接:链接外部库函数和资源。 | 生成最终的可执行文件或静态/动态库文件。 |
基础编译步骤
在开始交叉编译之前,了解基础的编译过程有助于理解后续步骤。
创建一个简单的C文件,文件名test.c ,内容如下:
c
#include <stdio.h>
int main() {
printf("Hello from Android NDK!\n");
return 0;
}
预处理阶段
预处理阶段处理 #include
、#define
等预处理指令。
bash
gcc -E test.c -o test.i
-E
:预处理后停止编译,输出预处理结果。
编译阶段
将预处理后的代码编译为汇编代码。
bash
gcc -S test.i -o test.s
-S
:编译后生成汇编代码。
汇编阶段
将汇编代码转换为目标文件。
bash
gcc -c test.s -o test.o
-c
:编译并汇编,生成目标文件,不进行链接。
链接阶段
将目标文件链接生成可执行文件。
bash
gcc test.o -o test
bash
./test
输出:
bash
Hello from Android NDK!
一步到位编译
将所有步骤合并为一步:
bash
gcc test.c -o test
./test
输出:
bash
Hello from Android NDK!
部署到 Android 设备
将编译后的test文件放到安卓设备中,看看能不能运行,使用以下命令推送到设备,赋予执行权限并运行:
bash
adb push test /data/local/tmp/
adb shell chmod +x /data/local/tmp/test
adb shell /data/local/tmp/test
不出意外的话,会输出以下内容:
bash
not executable: 64-bit ELF file
由于在 Windows 上编译的可执行文件使用的是 Windows 的二进制格式,无法在 Android 设备上运行。需要进行交叉编译。
注意: 上面简单的编译步骤,都是使用gcc进行编译,我使用的NDK版本为 27.1.12297006 ,谷歌已经移除了 gcc 工具链,转而完全使用 Clang!
交叉编译
下载和配置 Android NDK
在 Android NDK 下载页面 下载适用于 Windows 的 NDK。以 android-ndk-r27c 为例。
将下载的 NDK 解压到任意目录,例如 D:/Android/ndk/android-ndk-r27c。
添加环境
注意,在Ubuntu LTS 中,引用Windows的存储路径前面要加上 /mnt/
比如 /mnt/d/
就是代表D盘。
bash
export PATH=$PATH:/mnt/d/Android/ndk/android-ndk-r27c/toolchains/llvm/prebuilt/windows-x86_64/bin
验证
在 Ubuntu LTS 命令行输入一下指令,我没有使用引用路径方式,所以是很长一串,但也方便理解嘛:)
bash
/mnt/d/Android/ndk/android-ndk-r27c/toolchains/llvm/prebuilt/windows-x86_64/bin/aarch64-linux-android21-clang --version
出现类似下面输出,代表环境添加成功:
bash
Android (12470979, based on r522817c) clang version 18.0.3...
...windows-x86_64/bin/
路径下 有很多编译工具链,因为的测试机CPU架构为 arm64-v8a
所以我使用的编辑工具链为:aarch64-linux-android21-clang
,aarch64-linux-android21-clang
表示为 ARM64 架构和 API 级别 21 编译的工具链。这个要根据你实际的来,根据目标架构选择合适的工具链:
- aarch64-linux-android* 适用于 ARM64 架构设备。
- armv7a-linux-androideabi* 适用于 32 位 ARM 架构设备。
- x86_64-linux-android* 适用于 64 位 x86 架构设备。
- i686-linux-android* 适用于 32 位 x86 架构设备。
查看自己安卓设备的CPU架构:
bash
adb shell
getprop ro.product.cpu.abi
使用以下命令进行交叉编译:
bash
/mnt/d/Android/ndk/android-ndk-r27c/toolchains/llvm/prebuilt/windows-x86_64/bin/aarch64-linux-android21-clang -o test test.c
部署到 Android 设备
bash
adb push test /data/local/tmp/
adb shell chmod +x /data/local/tmp/test
adb shell /data/local/tmp/test
设备上应该输出以下内容:
生成静态库和动态库
除了生成可执行文件,NDK 还允许生成静态库(.a 文件)和动态库(.so 文件),以便在 Android 项目中使用。
使用一个新的test.c
文件,增加个返回值,方便在安卓中查看是否调用成功:
c
#include <stdio.h>
int test_function() {
printf("Hello from C!\n");
return 20241231;
}
生成静态库
使用 clang 编译生成静态库,运行以下命令生成 .a 文件:
bash
/mnt/d/Android/ndk/android-ndk-r27c/toolchains/llvm/prebuilt/windows-x86_64/bin/aarch64-linux-android21-clang -c -o test.o test.c
bash
ar rcs test.a test.o
-c
:编译为目标文件(不链接)。ar rcs
:将目标文件打包成静态库test.a
。
生成动态库
使用 clang 编译生成静态库,运行以下命令生成 .so 文件:
bash
/mnt/d/Android/ndk/android-ndk-r27c/toolchains/llvm/prebuilt/windows-x86_64/bin/aarch64-linux-android21-clang -fPIC -shared test.c -o libtest.so
-fPIC
: 生成与位置无关的代码(Position-Independent Code),动态库必须启用此选项。-shared
: 指定输出为共享库(动态库)。-o libtest.so
: 输出动态库的文件名。
验证文件类型
bash
file libtest.so
会出现类似输出,则代表成功:
这时候我们就得到了交叉编译后的静态库(test.a 文件)和动态库(libtest.so 文件)!
在 Android 项目中集成本地库
项目结构
bash
crosscompiler/
├── app/
│ └── src/
│ └── main/
│ ├── cpp/
│ │ ├── native-lib.cpp
│ │ └── CMakeLists.txt
│ └── jniLibs/
│ └── arm64-v8a/
│ ├── test.a
│ └── libtest.so
├── build.gradle
└── settings.gradle
build.gradle cmake 配置
bash
android {
defaultConfig {
externalNativeBuild {
cmake {
abiFilters 'arm64-v8a'
}
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt" // 指定 CMakeLists.txt 文件路径
version "3.18.1"
}
}
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs']
}
}
}
静态库 CMakeLists.txt 配置
在 app/src/main/cpp/CMakeLists.txt
文件中,配置 CMake 以集成本地库。
bash
cmake_minimum_required(VERSION 3.18.1)
# 设置项目名称
project("crosscompiler")
# 设置库路径,相对于 CMakeLists.txt 的位置
set(LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
# 调试信息,确保路径正确
message(STATUS "ANDROID_ABI: ${ANDROID_ABI}")
message(STATUS "LIB_DIR: ${LIB_DIR}")
message(STATUS "IMPORTED_LOCATION: ${LIB_DIR}/libtest.so")
# 导入静态库
add_library(test_a STATIC IMPORTED)
set_target_properties(test_a PROPERTIES IMPORTED_LOCATION ${LIB_DIR}/test.a)
# 导入共享库- 这种方式有问题,使用的时候报依赖路径问题,不知道什么原因,暂时解决不了!!!
#add_library(test_so SHARED IMPORTED)
#set_target_properties(test_so PROPERTIES IMPORTED_LOCATION ${LIB_DIR}/libtest.so)
# 添加目标库(native-lib 是 Android 项目中的本地库)
add_library(
native-lib
SHARED
native-lib.cpp
)
# 包含头文件目录
#include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
#target_include_directories(native-lib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 链接日志库和静态库
# 只能找系统的
find_library(log-lib log)
# 链接库
target_link_libraries(
native-lib
${log-lib}
test_a
)
- 静态库 native-lib.cpp:
bash
#include <jni.h>
#include <string>
#include <dlfcn.h>
#include <android/log.h>
#define LOG_TAG "native-lib"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
extern "C" int test_function();//定义静态库里的方法
extern "C"
JNIEXPORT jstring JNICALL
Java_com_xaye_crosscompiler_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
// 返回结果作为字符串
std::string returnValue = "Result from libtest.so test_function: " + std::to_string(test_function());
return env->NewStringUTF(returnValue.c_str());
}
- 调用:
bash
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.i("MainActivity", "stringFromJNI :"+stringFromJNI());
}
public native String stringFromJNI();
}
- 静态库运行:
动态库 CMakeLists.txt 配置
将静态库相关的代码注释,导入动态库 按网上介绍的 使用add_library(test_so SHARED IMPORTED)
,但是我在实际运行中会出现如下报错内容,搞了好久也没有解决,遂采用了动态加载方式。
bash
java.lang.UnsatisfiedLinkError: dlopen failed: library
"D:/dev/CrossCompiler/app/src/main/cpp/../jniLibs/arm64-v8a/libmylib.so" not found: needed by /data/app/~~CSB9pDlqQ4YQvRbO7pIzjQ==/com.xaye.crosscompiler-
3VV9X9lhsf7GkQ_fDT2P1w==/base.apk!/lib/arm64-v8a/libnative-lib.so in namespace classloader-namespace
bash
cmake_minimum_required(VERSION 3.18.1)
# 设置项目名称
project("crosscompiler")
# 批量引入源文件
file(GLOB allCpp *.cpp)
# 设置库路径,相对于 CMakeLists.txt 的位置
set(LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
# 调试信息,确保路径正确
message(STATUS "ANDROID_ABI: ${ANDROID_ABI}")
message(STATUS "LIB_DIR: ${LIB_DIR}")
message(STATUS "IMPORTED_LOCATION: ${LIB_DIR}/libtest.so")
# 导入静态库
#add_library(test_a STATIC IMPORTED)
#set_target_properties(test_a PROPERTIES IMPORTED_LOCATION ${LIB_DIR}/test.a)
# 导入共享库- 这种方式有问题,使用的时候报依赖路径问题,不知道什么原因,暂时解决不了!!!
#add_library(test_so SHARED IMPORTED)
#set_target_properties(test_so PROPERTIES IMPORTED_LOCATION ${LIB_DIR}/libtest.so)
# 添加目标库(native-lib 是 Android 项目中的本地库)
add_library(
native-lib
SHARED
native-lib.cpp
)
# 包含头文件目录
#include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)
#target_include_directories(native-lib PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 链接日志库和静态库
# 只能找系统的
find_library(log-lib log)
# 链接库
target_link_libraries(
native-lib
${log-lib}
#test_a
)
- 动态库 native-lib.cpp:
bash
#include <jni.h>
#include <string>
#include <dlfcn.h>
#include <android/log.h>
#define LOG_TAG "native-lib"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
// 定义函数指针类型
typedef int (*TestFunction)();
extern "C"
JNIEXPORT jstring JNICALL
Java_com_xaye_crosscompiler_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
// 动态加载 libtest.so
void* handle = dlopen("libtest.so", RTLD_LAZY);
if (!handle) {
LOGE("Failed to load libtest.so: %s", dlerror());
return env->NewStringUTF("Failed to load libtest.so");
}
// 清除之前的错误
dlerror();
// 获取函数指针
TestFunction test_function = (TestFunction)dlsym(handle, "test_function");
const char* dlsym_error = dlerror();
if (dlsym_error) {
LOGE("Failed to find test_function: %s", dlsym_error);
dlclose(handle);
return env->NewStringUTF("Failed to find test_function");
}
// 调用函数
int result = test_function();
LOGE("Result from test_function: %d", result);
// 关闭库
dlclose(handle);
// 返回结果作为字符串
std::string returnValue = "Result from libtest.so test_function: " + std::to_string(result);
return env->NewStringUTF(returnValue.c_str());
}
- 调用:
由于 native-lib.cpp
使用了动态加载,所以调用的代码和上面静态库的一样,只调用 System.loadLibrary("native-lib");
即可。
- 动态库库运行:
安卓中静态库与动态库的区别
在安卓中,静态库(Static Library)和动态库(Dynamic Library)是两种常见的代码复用方式。它们在链接方式、加载时机、文件格式以及应用场景等方面存在显著差异。
方面 | 静态库(Static Library) | 动态库(Dynamic Library) |
---|---|---|
文件扩展名 | .a (在Android NDK中) |
.so (Shared Object) |
链接方式 | 编译时链接,将库代码嵌入到最终的可执行文件或APK中。 | 运行时链接,库代码在应用运行时加载。 |
加载时机 | 在编译阶段将库代码整合到应用中,生成的APK包含所有必要的代码。 | 应用启动或运行过程中动态加载库,APK中仅包含库的引用。 |
文件大小 | 增加APK的体积,因为库代码被嵌入到每个使用它的应用中。 | 减少APK的体积,多个应用可以共享同一个动态库。 |
内存使用 | 每个使用静态库的应用都有一份独立的库代码,内存占用较高。 | 多个应用可以共享同一份动态库,节省内存。 |
更新与维护 | 更新静态库需要重新编译并发布所有依赖该库的应用。 | 更新动态库后,所有依赖该库的应用可以自动使用最新版本,无需重新编译。 |
性能 | 编译时链接,运行时无需加载,可能具有更高的执行效率。 | 运行时加载,可能引入轻微的加载延迟,但现代设备对此影响较小。 |
安全性 | 库代码被嵌入到应用中,增加了反编译的难度,但同样增加了APK的攻击面。 | 动态库单独存在,可能更易于管理权限和安全策略,但如果被替换或篡改,可能影响多个应用。 |
依赖管理 | 每个应用独立管理库的版本,避免版本冲突问题。 | 需要确保动态库的版本兼容性,避免"地狱依赖"问题。 |
使用场景 | 适用于需要高度集成且不频繁更新的库,如某些性能关键的模块。 | 适用于多个应用共享的库,或需要频繁更新和维护的库,如广告SDK、分析工具等。 |
根据需求,若需紧密集成和高性能且不共享,选择静态库;若需共享、便于更新及优化APK大小,选择动态库。
最后
以上就是NDK 交叉编译动态库、静态库全部基础流程,唯一让我疑惑的是为什么不能在 CMakeLists.txt 中进行链接动态库,问题大概是 libnative-lib.so
中引用了一个绝对的 Windows 路径 D:/dev/CrossCompiler/app/src/main/cpp/../jniLibs/arm64-v8a/libtest.so
,而 Android 设备无法识别该路径。但是...试了很多方法依旧不行,如果你知道答案,欢迎评论区留言,不吝赐教 !