跳转至

第16章 NDK与JNI开发

NDK与JNI开发图

学习目标:掌握Android NDK开发,理解JNI机制,实现Java/Kotlin与C/C++的交互。

预计学习时间:3-5天 实践时间:1-2天


目录

  1. NDK概述
  2. JNI基础
  3. CMake构建
  4. 性能优化
  5. 实践练习

1. NDK概述

1.1 什么是NDK

NDK(Native Development Kit)是一套工具集,允许开发者使用C/C++代码开发Android应用。

1.2 适用场景

  • ✅ 性能敏感计算(图像处理、音视频编解码)
  • ✅ 复用现有C/C++库
  • ✅ 跨平台代码共享
  • ✅ 底层硬件访问

2. JNI基础

2.1 声明Native方法

Kotlin
// 声明包含 native 方法的 Kotlin 类
class NativeLib {
    companion object {
        init {
            // 在类加载时加载名为 "native-lib" 的 C/C++ 动态库(对应 libnative-lib.so)
            System.loadLibrary("native-lib")
        }
    }

    // 使用 external 关键字声明由 C/C++ 实现的本地方法
    external fun stringFromJNI(): String       // 从 C++ 层获取字符串
    external fun add(a: Int, b: Int): Int      // 调用 C++ 层执行整数加法运算
}

2.2 C++实现

C++
// native-lib.cpp — JNI 本地方法实现文件
#include <jni.h>     // JNI 头文件,提供 JNIEnv、jstring 等类型定义
#include <string>    // C++ 标准字符串库

// extern "C" 防止 C++ 名称修饰,确保 JVM 能正确找到函数
// 函数命名规则:Java_包名_类名_方法名(包名中的 . 替换为 _)
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_nativelib_NativeLib_stringFromJNI(
        JNIEnv* env,          // JNI 环境指针,用于调用 JNI 函数
        jobject /* this */) { // 调用该方法的 Java 对象引用
    std::string hello = "Hello from C++";
    // 将 C++ 字符串转换为 Java 的 UTF 字符串并返回
    return env->NewStringUTF(hello.c_str());
}

// 实现 Kotlin 层声明的 add() 方法,接收两个 jint 参数
extern "C" JNIEXPORT jint JNICALL
Java_com_example_nativelib_NativeLib_add(
        JNIEnv* env,
        jobject /* this */,
        jint a,               // 对应 Kotlin 的 Int 类型
        jint b) {
    return a + b;             // 在 Native 层完成加法运算后返回结果
}

2.3 数据类型映射

Java/Kotlin JNI类型 C/C++类型
boolean jboolean unsigned char
byte jbyte signed char
char jchar unsigned short
short jshort short
int jint int
long jlong long long
float jfloat float
double jdouble double
String jstring -
Object jobject -

3. CMake构建

3.1 CMakeLists.txt

CMake
# 指定 CMake 最低版本要求
cmake_minimum_required(VERSION 3.22.1)
# 定义项目名称
project("native-lib")

# 创建共享库(.so 文件),SHARED 表示动态库
add_library(
    native-lib          # 库名称(生成 libnative-lib.so)
    SHARED              # 库类型:共享/动态库
    native-lib.cpp)     # 源文件列表

# 查找 Android 系统自带的 log 库,用于 __android_log_print 等日志函数
find_library(
    log-lib             # 变量名,存储找到的库路径
    log)                # 要查找的库名称

# 将 log 库链接到 native-lib,使其可以使用日志功能
target_link_libraries(
    native-lib          # 目标库
    ${log-lib})         # 要链接的依赖库

3.2 build.gradle配置

Kotlin
android {
    // 配置外部 CMake 构建脚本路径和版本
    externalNativeBuild {
        cmake {
            path = file("src/main/cpp/CMakeLists.txt")  // CMakeLists.txt 文件位置
            version = "3.22.1"                           // 指定 CMake 版本
        }
    }

    defaultConfig {
        externalNativeBuild {
            cmake {
                cppFlags += "-std=c++17"                  // 启用 C++17 标准
                arguments += "-DANDROID_STL=c++_shared"   // 使用共享的 C++ 标准库
            }
        }
        ndk {
            // 指定支持的 CPU 架构(ABI),覆盖主流 ARM 和 x86 平台
            abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
        }
    }
}

4. 性能优化

4.1 减少JNI调用开销

C++
// 批量处理数据——演示如何高效操作 Java 数组,减少 JNI 调用次数
JNIEXPORT void JNICALL
Java_processArray(JNIEnv* env, jobject thiz, jintArray arr) {
    // 获取 Java int 数组的本地指针,避免逐元素跨 JNI 边界访问
    jint* elements = env->GetIntArrayElements(arr, nullptr);
    // 获取数组长度
    jsize len = env->GetArrayLength(arr);

    // 在 Native 层批量处理所有元素(示例:每个元素乘以 2)
    for (int i = 0; i < len; i++) {
        elements[i] *= 2;
    }

    // 释放数组并将修改写回 Java 层(第三个参数 0 表示拷贝回去并释放本地副本)
    env->ReleaseIntArrayElements(arr, elements, 0);
}

4.2 使用Direct Buffer

C++
// 使用 Direct Buffer 实现零拷贝数据传输,避免 JNI 数组拷贝开销
JNIEXPORT void JNICALL
Java_processBuffer(JNIEnv* env, jobject thiz, jobject buffer) {
    // 获取 Java NIO DirectByteBuffer 的本地内存地址(无需数据拷贝)
    void* data = env->GetDirectBufferAddress(buffer);  // 指针:存储变量的内存地址
    // 获取 Buffer 的容量(字节数)
    jlong capacity = env->GetDirectBufferCapacity(buffer);

    // 直接在本地内存上操作数据,Java 层与 Native 层共享同一块内存
    processData(data, capacity);
}

5. 实践练习

练习1:图像处理

任务:实现灰度转换

要求: - 读取Bitmap - C++实现灰度算法 - 返回处理后的图像

练习2:加密算法

任务:实现AES加密

要求: - 使用OpenSSL - Java层调用 - 性能对比


本章小结

核心要点

  1. NDK用于C/C++开发
  2. JNI是Java与C/C++的桥梁
  3. CMake是推荐的构建工具
  4. 注意JNI调用开销

下一步

完成本章学习后,请进入第17章:安全与加密技术


本章完成时间:预计3-5天