Android NDK 开发

内容分享1天前发布
0 0 0

Android NDK(Native Development Kit)是一套工具集,允许你使用 C 和 C++ 等原生代码语言来开发 Android 应用的部分功能。

一、NDK 核心概念

1. 什么是 NDK?

定义:NDK 是 Android SDK 的补充工具集,用于将 C/C++ 代码编译成 
.so
(Shared Object,共享库)文件,供 Android 应用(Java/Kotlin)调用。核心作用
复用现有 C/C++ 代码(如算法库、游戏引擎、硬件驱动)。提升性能敏感场景(如实时渲染、音视频处理、加密计算)的执行效率。访问 Android 平台的底层 API(如 OpenSL ES 音频、OpenGL ES 图形、传感器)。 适用场景
高性能计算(如数学建模、图像处理)。跨平台开发(如 Unity、Cocos 游戏引擎)。硬件交互(如蓝牙、USB 设备通信)。代码保护(C/C++ 反编译难度高于 Java)。

2. 关键术语

JNI(Java Native Interface):Java 与 C/C++ 通信的桥梁,定义了一套规范,允许 Java 调用 C/C++ 函数,反之亦然。.so 文件:C/C++ 编译后的动态链接库,Android 按 CPU 架构(armeabi-v7a、arm64-v8a、x86、x86_64)生成对应版本。CMake:跨平台构建工具,用于定义 NDK 项目的编译规则(替代旧版 ndk-build)。NDK 工具链:包含编译器(clang)、链接器(ld)、调试器(gdb)等工具的集合。ABI(Application Binary Interface):应用二进制接口,定义了 CPU 架构、指令集、函数调用约定等,决定了 
.so
 文件的兼容性。

二、NDK 开发环境搭建

1. 安装 NDK 和 CMake

通过 Android Studio 安装
打开 Android Studio → 
File
 → 
Settings
 → 
Appearance & Behavior
 → 
System Settings
 → 
Android SDK
。切换到 
SDK Tools
 选项卡,勾选:
NDK (Side by side)(推荐最新稳定版)。CMakeLLDB(调试 C/C++ 代码用)。 点击 
Apply
 下载安装。

2. 配置项目的 
build.gradle

在模块级 
build.gradle

app/build.gradle
)中配置 NDK 相关参数:

groovy



android {
    compileSdk 34
 
    defaultConfig {
        applicationId "com.example.ndkdemo"
        minSdk 21
        targetSdk 34
        versionCode 1
        versionName "1.0"
 
        // 配置支持的 CPU 架构(按需添加)
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
        }
 
        // 配置 CMake 构建脚本路径
        externalNativeBuild {
            cmake {
                cppFlags "" // 可添加额外的 C++ 编译 flags(如 -std=c++17)
            }
        }
    }
 
    // 配置外部原生构建(CMake)
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt" // CMake 脚本路径
            version "3.22.1" // CMake 版本(需与安装的版本匹配)
        }
    }
 
    // 其他配置...
}

三、NDK 开发流程(Hello World 示例)

1. 创建 C/C++ 源文件

在 
app/src/main/
 下创建 
cpp
 目录,新建 
native-lib.cpp
 文件:

cpp

运行



#include <jni.h>
#include <string>
 
// JNI 函数:Java_包名_类名_方法名
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndkdemo_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    // 将 C++ string 转换为 Java String 并返回
    return env->NewStringUTF(hello.c_str());
}

说明

extern "C"
:确保函数名按 C 语言规则编译(避免 C++ 名称修饰,让 Java 能正确找到函数)。
JNIEXPORT
 和 
JNICALL
:JNI 关键字,标记该函数为外部可调用。
JNIEnv*
:JNI 环境指针,提供访问 Java 虚拟机的接口(如创建 Java 对象、调用 Java 方法)。
jobject
:对应 Java 中的 
this
(非静态方法),静态方法为 
jclass
(对应 Java 类)。

2. 编写 CMake 构建脚本(CMakeLists.txt)

在 
app/src/main/cpp/
 下创建 
CMakeLists.txt
,定义编译规则:

cmake



# 指定 CMake 最低版本
cmake_minimum_required(VERSION 3.22.1)
 
# 定义项目名称(可选)
project("ndkdemo")
 
# 添加源文件:将 native-lib.cpp 编译成动态库
add_library(
        # 动态库名称(需与 Java 中 System.loadLibrary("ndkdemo") 对应)
        ndkdemo
 
        # 库类型:SHARED 表示动态库(.so 文件)
        SHARED
 
        # 源文件路径
        native-lib.cpp)
 
# 查找 Android NDK 提供的日志库(用于在 C++ 中打印日志)
find_library(
        # 日志库变量名(后续链接时使用)
        log-lib
 
        # 日志库名称(Android NDK 内置库,无需手动添加源文件)
        log)
 
# 链接库:将 ndkdemo 库与日志库链接起来
target_link_libraries(
        # 目标库
        ndkdemo
 
        # 链接的库(${log-lib} 对应前面查找的日志库)
        ${log-lib})
3. 在 Java 中调用 C/C++ 函数

在 
MainActivity.java
 中加载 
.so
 库并声明 native 方法:

java

运行



package com.example.ndkdemo;
 
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
 
public class MainActivity extends AppCompatActivity {
    // 加载 C/C++ 编译后的动态库(库名与 CMakeLists.txt 中 add_library 的名称一致)
    static {
        System.loadLibrary("ndkdemo");
    }
 
    // 声明 native 方法(与 C++ 中函数名对应)
    public native String stringFromJNI();
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        // 调用 native 方法,获取 C++ 返回的字符串
        String result = stringFromJNI();
        TextView textView = findViewById(R.id.sample_text);
        textView.setText(result);
    }
}
4. 编译运行

点击 Android Studio 的「Run」按钮,NDK 会自动编译 C/C++ 代码生成 
.so
 文件,并打包到 APK 中。运行后,界面会显示 
Hello from C++
,表示调用成功。

四、JNI 核心交互(Java ↔ C/C++)

1. 数据类型映射

JNI 定义了 Java 类型与 C/C++ 类型的对应关系:

Java 类型 JNI 类型 C/C++ 类型(示例)
byte jbyte signed char
short jshort short
int jint int
long jlong long long
float jfloat float
double jdouble double
boolean jboolean unsigned char(0=false,1=true)
char jchar unsigned short
String jstring const char*(需转换)
int[] jintArray jint*(需转换)
Object jobject jobject(需通过 JNI 操作)
2. 字符串交互(Java String ↔ C/C++ char*)

C++ 接收 Java String

cpp

运行



extern "C" JNIEXPORT void JNICALL
Java_com_example_ndkdemo_MainActivity_passString(JNIEnv* env, jobject thiz, jstring str) {
    // 将 jstring 转换为 C++ char*(UTF-8 编码)
    const char* c_str = env->GetStringUTFChars(str, nullptr);
    if (c_str != nullptr) {
        // 使用 c_str(如打印日志)
        __android_log_print(ANDROID_LOG_INFO, "NDK", "Java 传递的字符串:%s", c_str);
        // 释放资源(避免内存泄漏)
        env->ReleaseStringUTFChars(str, c_str);
    }
}

C++ 返回 Java String

cpp

运行



extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndkdemo_MainActivity_returnString(JNIEnv* env, jobject thiz) {
    const char* c_str = "C++ 返回的字符串";
    // 将 C++ char* 转换为 jstring(UTF-8 编码)
    return env->NewStringUTF(c_str);
}
3. 数组交互(Java 数组 ↔ C/C++ 数组)

C++ 接收 Java 数组

cpp

运行



extern "C" JNIEXPORT void JNICALL
Java_com_example_ndkdemo_MainActivity_passIntArray(JNIEnv* env, jobject thiz, jintArray arr) {
    // 获取数组长度
    jsize length = env->GetArrayLength(arr);
    // 将 jintArray 转换为 C++ jint*(获取数组元素的指针)
    jint* c_arr = env->GetIntArrayElements(arr, nullptr);
    if (c_arr != nullptr) {
        // 操作数组(如打印所有元素)
        for (int i = 0; i < length; i++) {
            __android_log_print(ANDROID_LOG_INFO, "NDK", "数组元素 %d:%d", i, c_arr[i]);
        }
        // 释放资源(0 表示复制回 Java 数组并释放 C++ 数组)
        env->ReleaseIntArrayElements(arr, c_arr, 0);
    }
}

C++ 返回 Java 数组

cpp

运行



extern "C" JNIEXPORT jintArray JNICALL
Java_com_example_ndkdemo_MainActivity_returnIntArray(JNIEnv* env, jobject thiz) {
    jsize length = 5;
    // 创建 Java int 数组
    jintArray j_arr = env->NewIntArray(length);
    // 定义 C++ 数组
    jint c_arr[] = {1, 2, 3, 4, 5};
    // 将 C++ 数组复制到 Java 数组
    env->SetIntArrayRegion(j_arr, 0, length, c_arr);
    return j_arr;
}
4. 调用 Java 方法(C++ → Java)

cpp

运行



extern "C" JNIEXPORT void JNICALL
Java_com_example_ndkdemo_MainActivity_callJavaMethod(JNIEnv* env, jobject thiz) {
    // 1. 获取 Java 类(MainActivity.class)
    jclass clazz = env->GetObjectClass(thiz);
    if (clazz == nullptr) return;
 
    // 2. 查找 Java 方法(方法签名:(Ljava/lang/String;)V)
    // 方法签名格式:(参数类型)返回类型,String 对应 Ljava/lang/String;,void 对应 V
    jmethodID method_id = env->GetMethodID(clazz, "onNativeCall", "(Ljava/lang/String;)V");
    if (method_id == nullptr) {
        env->DeleteLocalRef(clazz); // 释放类引用
        return;
    }
 
    // 3. 准备方法参数(C++ char* → Java String)
    jstring param = env->NewStringUTF("C++ 调用 Java 方法");
 
    // 4. 调用 Java 方法(thiz 是调用对象,method_id 是方法 ID,param 是参数)
    env->CallVoidMethod(thiz, method_id, param);
 
    // 5. 释放资源
    env->DeleteLocalRef(param);
    env->DeleteLocalRef(clazz);
}

对应的 Java 方法:

java

运行



// 与 C++ 中 GetMethodID 的方法名、签名一致
public void onNativeCall(String message) {
    Log.d("NDK", "Java 收到 C++ 调用:" + message);
    // 可执行 UI 更新等操作
    runOnUiThread(() -> Toast.makeText(this, message, Toast.LENGTH_SHORT).show());
}

五、NDK 调试

1. 配置调试环境

在 
build.gradle
 中启用调试:

groovy



android {
    buildTypes {
        debug {
            debuggable true // 启用调试
            jniDebuggable true // 启用 JNI 调试
        }
    }
}

选择「Run」→「Edit Configurations」,确保 
Debugger
 选择「Auto」或「Native」。

2. 调试步骤

在 C/C++ 代码中设置断点(点击代码行号左侧)。以调试模式运行应用(点击「Debug」按钮)。当程序执行到断点时,会暂停在 Android Studio 的调试面板,可查看变量值、单步执行(F10)、步入函数(F11)等。

3. 日志调试

使用 Android NDK 内置的 
__android_log_print
 函数打印日志,在 Logcat 中查看:

cpp

运行



#include <android/log.h>
 
// 打印日志:级别(ANDROID_LOG_INFO)、标签("NDK")、内容(格式化字符串)
__android_log_print(ANDROID_LOG_INFO, "NDK", "调试信息:%d", 123);

日志级别:
ANDROID_LOG_VERBOSE
(V)、
ANDROID_LOG_DEBUG
(D)、
ANDROID_LOG_INFO
(I)、
ANDROID_LOG_WARN
(W)、
ANDROID_LOG_ERROR
(E)。

六、NDK 开发注意事项

1. 内存管理

JNI 引用管理
局部引用(如 
jstring

jobject
):通过 
env->New...
 创建,作用域为当前 JNI 函数,需通过 
env->DeleteLocalRef
 手动释放(避免内存泄漏)。全局引用:通过 
env->NewGlobalRef
 创建,作用域为整个应用,需通过 
env->DeleteGlobalRef
 释放(否则会导致内存泄漏)。 C/C++ 内存管理
使用 
new
/
malloc
 分配的内存,需通过 
delete
/
free
 手动释放(避免内存泄漏)。

2. 线程安全

JNI 函数默认不线程安全,若在多线程中调用 JNI 方法,需注意:
每个线程有独立的 
JNIEnv*
,不能跨线程共享。若需在后台线程调用 JNI,需通过 
AttachCurrentThread
 附加线程到 JVM,获取当前线程的 
JNIEnv*

cpp

运行



JNIEnv* env;
// 附加当前线程到 JVM(g_vm 是全局 JVM 指针,需在 JNI_OnLoad 中初始化)
if (g_vm->AttachCurrentThread(&env, nullptr) == JNI_OK) {
    // 使用 env 执行 JNI 操作
    g_vm->DetachCurrentThread(); // 分离线程(在线程退出前调用)
}
3. 兼容性问题

ABI 兼容性
不同 CPU 架构(armeabi-v7a、arm64-v8a 等)的 
.so
 文件不能混用,需为目标架构编译对应的版本。建议只保留必要的 ABI(如 
arm64-v8a
 和 
armeabi-v7a
),减少 APK 体积。 Android 版本兼容性
避免使用高版本 NDK 中新增的 API,若需使用,需通过 
__ANDROID_API__
 判断 Android 版本:

cpp

运行



#if __ANDROID_API__ >= 24
    // 使用 Android 7.0(API 24)及以上的 API
#else
    // 兼容低版本的替代实现
#endif
4. 性能优化

减少 JNI 调用:JNI 调用有一定开销,避免在循环中频繁调用 JNI 方法(可批量传递数据)。使用原生数据类型:优先使用 JNI 定义的原生类型(如 
jint

jlong
),避免类型转换开销。优化内存拷贝:使用 
GetDirectBufferAddress
 直接访问 Java NIO 缓冲区,避免数据拷贝。

七、常用 NDK 工具

ndk-build:旧版 NDK 构建工具(基于 Makefile),现已逐步被 CMake 替代。CMake:跨平台构建工具,NDK 官方推荐使用。LLDB:Android Studio 内置的原生调试器,用于调试 C/C++ 代码。ndk-gdb:命令行调试工具,适用于无 Android Studio 的场景。objdump/
readelf
:查看 
.so
 文件的符号表、依赖库等信息。ndk-stack:解析原生崩溃日志(
 tombstone
 文件),定位崩溃位置。

总结

Android NDK 开发的核心是通过 JNI 实现 Java 与 C/C++ 的交互,适用于高性能、跨平台、底层交互等场景。实际开发中需注意内存管理、线程安全、兼容性和性能优化,同时熟练使用 CMake 构建和 LLDB 调试工具。随着 Android 平台的发展,NDK 也在不断更新,建议关注官方文档(Android NDK 官方指南)获取最新特性和最佳实践。

© 版权声明

相关文章

暂无评论

none
暂无评论...