使用安卓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 终端,执行以下命令:
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 ,内容如下:

#include <stdio.h>

int main() {
    printf("Hello from Android NDK!\n");
    return 0;
}

预处理阶段

预处理阶段处理 #include#define 等预处理指令。

gcc -E test.c -o test.i
  • -E:预处理后停止编译,输出预处理结果。

编译阶段

将预处理后的代码编译为汇编代码。

gcc -S test.i -o test.s
  • -S:编译后生成汇编代码。

汇编阶段

将汇编代码转换为目标文件。

gcc -c test.s -o test.o
  • -c:编译并汇编,生成目标文件,不进行链接。

链接阶段

将目标文件链接生成可执行文件。

gcc test.o -o test
./test

输出:

Hello from Android NDK!

一步到位编译

将所有步骤合并为一步:

gcc test.c -o test
./test

输出:

Hello from Android NDK!

部署到 Android 设备

将编译后的test文件放到安卓设备中,看看能不能运行,使用以下命令推送到设备,赋予执行权限并运行:

adb push test /data/local/tmp/
adb shell chmod +x /data/local/tmp/test
adb shell /data/local/tmp/test

不出意外的话,会输出以下内容:

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盘。

export PATH=$PATH:/mnt/d/Android/ndk/android-ndk-r27c/toolchains/llvm/prebuilt/windows-x86_64/bin

验证

在 Ubuntu LTS 命令行输入一下指令,我没有使用引用路径方式,所以是很长一串,但也方便理解嘛:)

/mnt/d/Android/ndk/android-ndk-r27c/toolchains/llvm/prebuilt/windows-x86_64/bin/aarch64-linux-android21-clang --version

出现类似下面输出,代表环境添加成功:

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架构:

adb shell
getprop ro.product.cpu.abi

使用以下命令进行交叉编译:

/mnt/d/Android/ndk/android-ndk-r27c/toolchains/llvm/prebuilt/windows-x86_64/bin/aarch64-linux-android21-clang -o test test.c

部署到 Android 设备

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文件,增加个返回值,方便在安卓中查看是否调用成功:

#include <stdio.h>

int test_function() {
    printf("Hello from C!\n");
    return 20241231;
}

生成静态库

使用 clang 编译生成静态库,运行以下命令生成 .a 文件:

/mnt/d/Android/ndk/android-ndk-r27c/toolchains/llvm/prebuilt/windows-x86_64/bin/aarch64-linux-android21-clang -c -o test.o test.c
ar rcs test.a test.o
  • -c:编译为目标文件(不链接)。
  • ar rcs:将目标文件打包成静态库 test.a

生成动态库

使用 clang 编译生成静态库,运行以下命令生成 .so 文件:

/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: 输出动态库的文件名。

验证文件类型

file libtest.so

会出现类似输出,则代表成功:
在这里插入图片描述
这时候我们就得到了交叉编译后的静态库(test.a 文件)和动态库(libtest.so 文件)!


在 Android 项目中集成本地库

项目结构

crosscompiler/
├── app/
│   └── src/
│       └── main/
│           ├── cpp/
│           │   ├── native-lib.cpp
│           │   └── CMakeLists.txt
│           └── jniLibs/
│               └── arm64-v8a/
│                   ├── test.a
│                   └── libtest.so
├── build.gradle
└── settings.gradle

build.gradle cmake 配置

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 以集成本地库。

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:
#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());
}
  • 调用:
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),但是我在实际运行中会出现如下报错内容,搞了好久也没有解决,遂采用了动态加载方式。

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
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:
#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 设备无法识别该路径。但是…试了很多方法依旧不行,如果你知道答案,欢迎评论区留言,不胜感激 !在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值