使用安卓NDK 交叉编译动态库、静态库基础入门

前言

本篇博客将详细介绍如何在 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 进行交叉编译至关重要。

  1. 启用 WSL 功能: 在 Windows 搜索栏中输入"启用或关闭 Windows 功能",然后勾选"适用于 Linux 的 Windows 子系统"选项。点击"确定"并重启电脑。

安装 Ubuntu

  1. 安装 Ubuntu: 打开 Microsoft Store,搜索并安装 Ubuntu 18.04 或更高版本。

  2. 初始化 Ubuntu: 安装完成后,启动 Ubuntu,设置用户名和密码。

安装 GCC 和 G++

  1. 更新包列表并安装构建工具: 打开 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 设备无法识别该路径。但是...试了很多方法依旧不行,如果你知道答案,欢迎评论区留言,不吝赐教 !

相关推荐
@OuYang6 小时前
android10 audio音量曲线
android
三爷麋了鹿11 小时前
VNC Viewer安卓版安装与操作
android
起个随便的昵称12 小时前
安卓入门十一 常用网络协议四
android·网络
BoomHe12 小时前
Android 车载性能优化-内存泄漏
android
起个随便的昵称13 小时前
安卓入门十三 常用功能模块一RxJava
android
工程师老罗13 小时前
Android笔试面试题AI答之Android基础(11)
android
叶羽西13 小时前
Android Camera压力测试工具
android
java_t_t13 小时前
安卓触摸事件的传递
android·java
千里马学框架15 小时前
千里马2024年终总结-android framework实战
android·framework·input·车机车载
tmacfrank16 小时前
Kotlin 协程基础知识总结五 —— 通道、多路复用、并发安全
android·开发语言·kotlin