当前位置: 首页 > news >正文

学习 Android(十四)NDK基础

学习 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-libSHAREDnative-lib.cpp
)find_library( # 找到 log 库log-liblog
)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-libSHAREDnative-lib.cpp
)find_library( # 引用 Android 日志库(可选)log-liblog
)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;@Overrideprotected 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 类型描述
booleanjboolean无符号 8 位(通常为 unsigned char),
值为 JNI_TRUE (1)JNI_FALSE (0)
bytejbyte有符号 8 位
charjchar无符号 16 位
shortjshort有符号 16 位
intjint有符号 32 位
longjlong有符号 64 位
floatjfloat32 位 IEEE 浮点数
doublejdouble64 位 IEEE 浮点数
voidvoid对应 void 类型

4.2 引用类型

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

4.3 特殊辅助类型

JNI 类型定义用途
jsizetypedef 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 类型编码
intI
booleanZ
byteB
charC
shortS
longJ
floatF
doubleD
voidV
ObjectL类名;
int[][I
StringLjava/lang/String;

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

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

类型Java -> NativeNative -> Java
Stringjstringconst 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 查看日志了

http://www.dtcms.com/a/318384.html

相关文章:

  • OpenWebUI通过pipeline对接dify的workflow
  • 滑动窗口相关题目
  • VirtualBox 搭建 Linux 虚拟机全流程:Nginx 学习环境前置配置指南
  • ##Anolis OS 8.10 安装oracle19c
  • 广州汽车配件3d打印模型加工厂家-中科米堆CASAIM
  • 【计组】存储系统
  • 3479. 水果成篮 III
  • Tiny-cuda-nn安装指南
  • CVE-2021-1879
  • Linux系统编程——环境变量、命令行参数
  • Dart语言语法与技术重点
  • 数据结构—队列和栈
  • openGauss单实例安装
  • YOLOv11改进:集成FocusedLinearAttention与C2PSA注意力机制实现性能提升
  • Redis使用的常见问题及初步认识
  • PLC学习之路-数据类型与地址表示-(二)
  • WinXP配置一键还原的方法
  • 【golang面试题】Golang递归函数完全指南:从入门到性能优化
  • 五十二、【Linux系统shell脚本】正则表达式演示
  • 202506 电子学会青少年等级考试机器人五级实际操作真题
  • 数据结构:栈、队列
  • C语言的数组与字符串练习题1
  • 18650电池组PACK自动化生产线:高效与品质的融合
  • 动物AI识别摄像头语音对讲功能
  • 大模型客户端工具如Cherry Studio,Cursor 配置mcp服务,容易踩的坑,总结
  • RPC框架之Kitex
  • 云手机和云真机之间存在的不同之处有什么?
  • [Oracle] LPAD()和RPAD()函数
  • Python实现电商商品数据可视化分析系统开发实践
  • 一、Istio基础学习