学习 Android(十四)NDK基础

发布于:2025-08-09 ⋅ 阅读:(16) ⋅ 点赞:(0)

学习 Android(十四)NDK基础

Android NDK 是一个工具集,可让我们使用 C 和 C++ 等语言以原生代码实现应用的各个部分。对于特定类型的应用,这可以帮助我们重复使用以这些语言编写的代码库。

接下来,我们将按照以下步骤进行讲解

  • NDK 是什么,作用和原理
  • Android Studio 中配置 NDK 与 CMake
  • 创建简单 Native 库(C/C++),Java 调用 Native 方法
  • 了解 JNI 基本概念,基本数据类型映射,Java 和 C++ 函数签名
  • 学习如何传递 Java 字符串、数组到 Native ,反之亦然

1. NDK 是什么?作用和原理

1.1 NDK 是什么?

原生开发套件 (NDK) 是一套工具,能够让我们在 Android 应用中使用 C 和 C++ 代码,并提供众多平台库,我们可使用这些平台库管理原生 activity 和访问实体设备组件,例如传感器和触控输入。NDK 可能不适合大多数 Android 编程初学者(例如作者我),初学者只需使用 Java 代码和框架 API 开发应用。然而,我们需要实现以下一个或多个目标,那么 NDK 就能派上用场:

  • 进一步提升设备性能,以降低延迟或运行游戏或物理模拟等计算密集型应用。

  • 重复使用您自己或其他开发者的 C 或 C++ 库。

我们可以在 Android Studio 2.2 或更高版本中使用 NDK 将 C 和 C++ 代码编译到原生库中,然后使用 Android Studio 的集成构建系统 Gradle 将原生库打包到 APK 中。Java 代码随后可以通过 Java 原生接口 (JNI) 框架调用原生库中的函数。

Android Studio 编译原生库的默认构建工具是 CMake。由于很多现有项目都使用 ndk-build 构建工具包,因此 Android Studio 也支持 ndk-build。不过,如果要创建新的原生库,则应使用 CMake。

1.2 NDK 的工作原理

NDK 的本质是通过 JNI(Java Native Interface)桥接 Java/Kotlin 和 C/C++ 本地代码,从而实现跨语言通信与调用,并在 Android 系统中生成 .so 动态链接库供运行时加载。

  • 整体架构流程图如下所示
Java/Kotlin 层
    |
    | 调用 native 方法
    v
JNI (Java Native Interface)
    |
    | 负责参数类型转换、方法注册
    v
C/C++ 层代码(通过 NDK 编译)
    |
    | 编译为 .so 动态库
    v
libnative-lib.so 被 Android 加载并运行
  • Java 层声明 native 方法

    我们首先要在 Java 或 Kotlin 中用 native 关键字声明一个方法:

    public class NativeLib {
        static {
            System.loadLibrary("native-lib"); // 加载 C/C++ 编译生成的 .so 文件
        }
    
        public native int add(int a, int b); // native 方法,C/C++ 实现
    }
    
  • C/C++ 层实现(通用JNI)

    我们需要在 C/C++ 中用 JNI 方式实现这个方法,签名必须完全匹配

    extern "C" JNIEXPORT jint JNICALL
    Java_com_example_NativeLib_add(JNIEnv *env, jobject thisz, jnit a, jint b) {
        return a + b;
    }
    
    • JNIEnv* 是 JNI 环境指针(用于访问 Java)

    • jobject 是 Java 传进来的对象引用(即 this)

  • 构建和变异位动态库(.so 文件)

    使用 CMakeLists.txtAndroid.mk 构建规则,把你的 C++ 文件编译成 .so

    • 输出目录:app/build/intermediates/cmake/debug/obj/arm64-v8a/libnative-lib.so

    • 会被打包进 APK,在运行时由 System.loadLibrary 加载

  • 运行时调用流程

    • 用户点击或代码调用 NativeLib.add()

    • JVM 会通过 JNI 找到 .so 文件中注册的 Java_com_example_NativeLib_add() 方法

    • 调用 C++ 实现,返回结果给 Java

2. Android Studio 中配置 NDK 与 CMake

2.1 在 Android Studio 中操作:

  1. 打开 Preferences(设置)

    • macOS: Android Studio > Preferences

    • Windows/Linux: File > Settings

  2. 导航到:
    Appearance & Behavior > System Settings > Android SDK > SDK Tools

  3. 勾选并安装:

    • NDK (Side by side)

    • CMake

    • LLDB(可选,调试 C++ 用)

2.2 配置 build.gradle 文件

以下以 App 模块的 build.gradle(Groovy 版) 为例说明配置方式:

  1. defaultConfig 中添加:

    defaultConfig {
        ...
    
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
    
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a' // 你可以根据需求精简架构
        }
    }
    
  2. 配置 externalNativeBuild

    android {
        ...
    
        externalNativeBuild {
            cmake {
                path "src/main/cpp/CMakeLists.txt" // 指向你的 CMake 配置文件
                version "3.22.1" // 根据你安装的版本写
            }
        }
    }
    

2.3 创建 C/C++ 文件和 CMake 配置

app/
 └── src/
      └── main/
           ├── cpp/
           │    ├── native-lib.cpp
           │    └── CMakeLists.txt
           └── java/

native-lib.cpp

#include <jni.h>

extern "C"
JNIEXPORT jint JNICALL
Java_com_example_ndkdemo_NativeLib_add(JNIEnv *env, jobject obj, jint a, jint b) {
    return a + b;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.10.2)

project("ndkdemo")

add_library( # 构建库名
    native-lib
    SHARED
    native-lib.cpp
)

find_library( # 找到 log 库
    log-lib
    log
)

target_link_libraries( # 链接 log 库
    native-lib
    ${log-lib}
)

2.4 Java 层调用 native 方法

public class NativeLib {
    static {
        System.loadLibrary("native-lib"); // 加载 .so
    }

    public native int add(int a, int b); // 声明 native 方法
}

2.5 构建与运行

  1. 点击 Build → Rebuild Project

  2. .so 文件将生成在:
    app/build/intermediates/cmake/debug/obj/arm64-v8a/libnative-lib.so

  3. 如果你运行到 ARM64 模拟器或真机,程序会自动加载对应 .so 并调用你的 native 方法。

3. 创建简单 Native 库(C/C++),Java 调用 Native 方法

3.1 步骤一:项目结构准备

在 Android Studio 中新建一个空项目(Empty Activity),选择 Java语言,API 21,然后按如下结构添加文件:

app/
 └── src/
      └── main/
           ├── cpp/
           │    ├── native-lib.cpp      C++ 实现文件
           │    └── CMakeLists.txt      CMake 构建文件
           └── java/com/example/ndkdemo/
                └── NativeLib.java      Java 调用封装类

3.2 步骤二:配置 build.gradle (app 模块)

android {
    defaultConfig {
        ...

        // 指定使用的 ABI 架构
        ndk {
            abiFilters 'armeabi-v7a', 'arm64-v8a'
        }

        // 配置 CMake 构建
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
    }

    // 指定 CMake 构建文件路径
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
        }
    }
}

3.3 步骤三:创建 CMake 构建文件(CMakeLists.txt)

app/src/main/cpp/CMakeLists.txt 中实现

cmake_minimum_required(VERSION 3.10.2)
project("ndkdemo") // 记得这是填对应的名称

add_library( # native 库名
    native-lib
    SHARED
    native-lib.cpp
)

find_library( # 引用 Android 日志库(可选)
    log-lib
    log
)

target_link_libraries(
    native-lib
    ${log-lib}
)

3.4 步骤四:实现 C++ 代码(native-lib.cpp)

app/src/main/cpp/native-lib.cpp 中实现

#include <jni.h>

// 使用 extern "C" 避免 C++ 方法名被改写(mangling)
extern "C"
JNIEXPORT jint JNICALL
Java_com_example_ndkdemo_NativeLib_add(JNIEnv *env, jobject thiz, jint a, jint b) {
    return a + b;
}

3.5 步骤五:Java 封装 Native 调用

com/example/ndkdemo/NativeLib.java 中实现

package com.example.ndkdemo;

public class NativeLib {
    static {
        System.loadLibrary("native-lib"); // 加载 native-lib.so 动态库
    }

    // native 方法声明,由 C++ 实现
    public native int add(int a, int b);
}

3.6 步骤六:在 Activity 中调用验证

com/example/ndkdemo/MainActivity.java 中实现

public class MainActivity extends AppCompatActivity {
    private final NativeLib nativeLib = new NativeLib();
    private TextView textView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        textView = findViewById(R.id.tv_result);

        int result = nativeLib.add(3, 4); // 调用 native 方法
        textView.setText("3 + 4 = " + result);
    }
}

3.7 步骤七:构建运行

编译运行结果如下所示
在这里插入图片描述

4. 了解 JNI 基本概念,基本数据类型映射,Java 和 C++ 函数签名

接下来我们将针对 JNI 进行相关的学习和了解

4.1 基本数据类型

Java 类型 JNI 类型 描述
boolean jboolean 无符号 8 位(通常为 unsigned char),
值为 JNI_TRUE (1)JNI_FALSE (0)
byte jbyte 有符号 8 位
char jchar 无符号 16 位
short jshort 有符号 16 位
int jint 有符号 32 位
long jlong 有符号 64 位
float jfloat 32 位 IEEE 浮点数
double jdouble 64 位 IEEE 浮点数
void void 对应 void 类型

4.2 引用类型

Java 类型 JNI 类型 说明
java.lang.Object jobject 所有对象的基类
任意 Java 类 jclass Java 类的引用
java.lang.String jstring Java 字符串
T[](Java 数组) jarray 所有数组的基类
原始类型数组 jintArrayjbyteArray 特定类型的数组
Java 对象数组 jobjectArray 包含对象引用的数组
异常 jthrowable 可被 throw 的对象

4.3 特殊辅助类型

JNI 类型 定义 用途
jsize typedef jint jsize; 表示数组、字符串长度或大小等
jfieldID 不透明指针类型 标识一个类的字段
jmethodID 不透明指针类型 标识一个类的方法

4.4 本地方法接口类型

JNI 提供的所有函数都通过这两个结构体访问:

类型名 说明
JNIEnv * 每个线程独有,包含 JNI 所有函数指针
JavaVM * JVM 实例指针,用于跨线程附加线程等操作

4.5 布尔常量

为兼容 C 语言布尔类型,定义了:

#define JNI_TRUE  1
#define JNI_FALSE 0

4.6 原始类型数组

Java 类型 JNI 类型
boolean[] jbooleanArray
byte[] jbyteArray
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray

4.7 对象数组

Java 类型 JNI 类型 说明
String[] jobjectArray 指向一组 jstring 对象的数组
Object[] jobjectArray 可存放任意引用类型对象
SomeClass[] jobjectArray 存放 SomeClass 对象的数组

5 Java 和 C++ 函数签名

Java 和 C++ 函数签名是函数唯一身份的定义方式,但两者的表现形式和规则存在差异。

5.1 Java 的函数签名

Java 中函数签名包括:函数名 + 参数类型列表(不包括返回值)

public int add(int a, int b) { ... }

Java 中,下面两个方法签名相同,会报错

public void test(int x) { }
public int tes(int x) { } // 编译报错:签名冲突(返回值不算签名)

Java 方法签名示例(包括参数类型):

方法声明 签名(方法名 + 参数类型)
void foo(int x) foo(I)
void foo(String s) foo(Ljava/lang/String;)
void foo(int[] arr) foo([I)
void foo(int x, String s) foo(ILjava/lang/String;)

5.2 C++ 的函数签名

C++ 中函数签名包括:函数名 + 参数类型列表(返回值不计入签名)

int add(int a, int b);
double add(int a, int b); // 编译错误:重定义函数(签名冲突)

但和 Java 不同的是,C++ 支持函数重载:C++ 的重载机制在编译和链接层处理得很好,不需要额外区分。但 Java 的重载虽然语法上支持,但在调用 native 方法时,需要开发者显式编码函数签名,这让处理重载略显麻烦。并不是说 Java 不支持重载,而是说 Java 的重载不天然适用于 native binding,需要额外工作。

void print(int x);
void print(double x);

函数签名还包括是否为指针、引用、常量等修饰:

void func(int &x); // 引用
void func(const int x); // const 修饰不同参数,签名不同

5.3 Java 和 C++ 在 JNI 中的函数签名映射

JNI 中为了让 Java 调用 C/C++ 函数,会将 Java 方法 签名映射为 JNI 名字。

public class MyClass {
    public native void hello(String msg);
}

对应的 C 函数签名为:

JNIEXPORT void JNICALL Java_MyClass_hello(JNIENV *env, jobject obj, jstring msg);

规则如下:

  • 包名和类名中的 . 替换为 _

  • 方法名拼接在类名后

  • 参数类型在 JNI 中通过 jintjstringjbooleanArray 等类型传递

5.4 常见 JNI 签名编码表

Java 类型 JNI 类型编码
int I
boolean Z
byte B
char C
short S
long J
float F
double D
void V
Object L类名;
int[] [I
String Ljava/lang/String;

6. 学习如何传递 Java 字符串、数组到 Native ,反之亦然

6.1 Java 与 Native(C/C++)之间的数据传递总览

类型 Java -> Native Native -> Java
String jstringconst char*(使用 GetStringUTFChars 创建 jstring(用 NewStringUTF
基本类型数组 jintArray, jbyteArray 等 → jint*(使用 GetXxxArrayElementsGetXxxArrayRegion 创建数组并填充(用 NewXxxArray + SetXxxArrayRegion
对象数组 jobjectArray → 单个元素用 GetObjectArrayElement 访问 创建 jobjectArray 并填充每一项
自定义对象 传入 jobject,通过 JNI API 访问其字段或方法 构造 Java 对象并返回

6.2 Java 字符串与 Native 的相互转换:

  • Java -> Native :获取 C 字符串:

    extern "C" JNIEXPORT void JNICALL
    Java_com_example_hello_NativeLib_print(JNIEnv* env, jobject thiz, jstring jStr) {
        const char* cStr = (*env).GetStringUTFChars(jStr, 0);
        printf("收到字符串: %s\n", cStr);
        (*env).ReleaseStringUTFChars(jStr, cStr); // 一定要释放
    }
    
  • Native -> Java :创建 Java 字符串:

    extern "C" JNIEXPORT jstring JNICALL
    Java_com_example_hello_NativeLib_stringFromJNI(
            JNIEnv* env,
            jobject thiz /* this */) {
        jstring result = (*env).NewStringUTF("你好 MainActivity");
        return result;
    }
    

6.3 Java 数组与 Native 的相互转换:

  • Java int[] -> Native

    extern "C" JNIEXPORT void JNICALL
    Java_com_example_demo_NativeLib_sum(JNIEnv* env, jobject thiz, jintArray arr) {
        jsize len = (*env).GetArrayLength(arr);
        jint* elems = (*env).GetIntArrayElements(arr, NULL);
        int sum = 0;
        for (int i = 0; i < len; i++) {
            sum += elems[i];
        }
        printf("总和: %d\n", sum);
        (*env).ReleaseIntArrayElements(arr, elems, 0); // 0 表示更新 Java 数组
    }
    
    
    
  • Native int[] -> Java int[]

    extern "C" JNIEXPORT jintArray JNICALL
    Java_com_example_demo_NativeLib_getNumbers(JNIEnv *env, jobject) {
        jint nums[] = {1, 2, 3, 4, 5};
        jintArray arr = (*env).NewIntArray(5);
        (*env).SetIntArrayRegion(arr, 0, 5, nums);
        return arr;
    }
    

在 Native (C/C++) 中使用 printf() 打印日志时,它的输出位置取决于哪个平台运行,在 Android 中 printf() 输出不会自动出现在 Logcat,我们通常看不到它的输出。

为此我们需要使用 __android_log_print

native-lib.cpp 中添加

#include <android/log.h>

#define LOG_TAG "NativeLog"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

将在 Native 中使用 print() 的方法替换成 LOGI() 或者 LOGE() 方法,我们就可以在 Logcat 查看日志了
jintArray arr = (*env).NewIntArray(5);
(*env).SetIntArrayRegion(arr, 0, 5, nums);
return arr;
}




在 Native (C/C++) 中使用 `printf()` 打印日志时,它的输出位置取决于哪个平台运行,**在 Android 中 `printf()` 输出不会自动出现在 Logcat**,我们通常看不到它的输出。

为此我们需要使用 `__android_log_print`

在 `native-lib.cpp` 中添加

```cpp
#include <android/log.h>

#define LOG_TAG "NativeLog"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

将在 Native 中使用 print() 的方法替换成 LOGI() 或者 LOGE() 方法,我们就可以在 Logcat 查看日志了


网站公告

今日签到

点亮在社区的每一天
去签到