Android Studio C++/JNI/Kotlin 示例 二
MainActivity.kt
package com.demo.learn1import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivityclass MainActivity : ComponentActivity() {// 加载原生库init {System.loadLibrary("native_code")}// 声明原生方法// 数学运算private external fun computeFactorial(n: Int): Intprivate external fun computeFibonacci(n: Int): Int// 字符串处理private external fun reverseString(input: String): Stringprivate external fun countVowels(input: String): Int// 数组处理private external fun sumIntArray(array: IntArray): Int// 复杂对象处理data class User(val name: String, val age: Int)private external fun processUserData(user: User): Stringoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 计算5的阶乘和斐波那契数列第10项Log.d("Native", "5! = ${computeFactorial(5)}")Log.d("Native", "fib(10) = ${computeFibonacci(10)}")// 测试字符串反转和元音计数功能val testStr = "Hello, JNI!"Log.d("Native", "Reversed: ${reverseString(testStr)}")Log.d("Native", "Vowel count: ${countVowels(testStr)}")// 计算数组元素的和val numbers = intArrayOf(1, 2, 3, 4, 5)Log.d("Native", "Array sum: ${sumIntArray(numbers)}")// 创建User对象并传递给本地方法处理val user = User("张三", 25)Log.d("Native", processUserData(user))}
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1) #指定 CMake 的最低版本要求(这里是 3.4.1,Android NDK 的常见要求)
set(CMAKE_CXX_STANDARD 11) # 启用C++11支持# 定义库和源文件
add_library( # 设置库名称native_code# 设置库类型为共享库SHARED# 提供源文件的相对路径jni_interface.cppmath_operations.cppstring_processor.cppdata_converter.cpp
)# 查找日志库
find_library( # 设置路径变量名称log-lib# 指定CMake要查找的NDK库名称log )# 指定库应该链接到的目标库
target_link_libraries( # 指定目标库native_code# 将目标库链接到日志库${log-lib} )
jni_interface.cpp
这段代码是用C++实现的JNI(Java Native Interface)接口,它连接了Java/Kotlin代码和本地C++代码。
#include "jni_interface.h"
#include <jni.h> //JNI核心头文件,提供与Java交互的所有必要定义
#include <string> //C++标准字符串库
#include <android/log.h> //Android日志输出功能
// 包含自定义类头文件
#include "math_operations.h"
#include "string_processor.h"
#include "data_converter.h"
//定义了日志宏LOGI,方便输出调试信息
#define LOG_TAG "NativeCode"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)extern "C" {// 数学运算示例
JNIEXPORT jint JNICALL
Java_com_demo_learn1_MainActivity_computeFactorial(JNIEnv* env,jobject /* this */,jint n) {return math_operations::factorial(n);
}JNIEXPORT jint JNICALL
Java_com_demo_learn1_MainActivity_computeFibonacci(JNIEnv* env,jobject /* this */,jint n) {return math_operations::fibonacci(n);
}// 字符串处理示例
JNIEXPORT jstring JNICALL
Java_com_demo_learn1_MainActivity_reverseString(JNIEnv* env,jobject /* this */,jstring input) {std::string cppStr = data_converter::convertJavaStringToCpp(env, input);std::string reversed = string_processor::reverseString(cppStr);return env->NewStringUTF(reversed.c_str());
}JNIEXPORT jint JNICALL
Java_com_demo_learn1_MainActivity_countVowels(JNIEnv* env,jobject /* this */,jstring input) {std::string cppStr = data_converter::convertJavaStringToCpp(env, input);return string_processor::countVowels(cppStr);
}// 数组处理示例
JNIEXPORT jint JNICALL
Java_com_demo_learn1_MainActivity_sumIntArray(JNIEnv* env,jobject /* this */,jintArray array) {std::vector<int> numbers = data_converter::convertJavaArrayToVector(env, array);int sum = 0;for (int num : numbers) {sum += num;}return sum;
}// 复杂对象示例
JNIEXPORT jstring JNICALL
Java_com_demo_learn1_MainActivity_processUserData(JNIEnv* env,jobject /* this */,jobject user) {jclass userClass = env->GetObjectClass(user);// 获取字段IDjfieldID nameField = env->GetFieldID(userClass, "name", "Ljava/lang/String;");jfieldID ageField = env->GetFieldID(userClass, "age", "I");// 获取字段值jstring name = (jstring)env->GetObjectField(user, nameField);jint age = env->GetIntField(user, ageField);// 处理数据std::string cppName = data_converter::convertJavaStringToCpp(env, name);std::string processed = "User: " + cppName + ", Age: " + std::to_string(age);// 清理引用env->DeleteLocalRef(name);env->DeleteLocalRef(userClass);return env->NewStringUTF(processed.c_str());
}} // extern "C"
processUserData方法讲解
参数:
JNIEnv* env
:JNI环境指针,提供所有JNI功能jobject this
:Java中调用该native方法的对象实例(本例未使用)jobject user
:传入的Java User对象
1.获取Java类信息
jclass userClass = env->GetObjectClass(user);
GetObjectClass
:获取传入Java对象的类对象返回的
jclass
是Java中Class<User>
的本地表示这是后续访问字段和方法的基础
2. 获取字段ID
jfieldID nameField = env->GetFieldID(userClass, "name", "Ljava/lang/String;");
jfieldID ageField = env->GetFieldID(userClass, "age", "I");
GetFieldID
:获取字段标识符,需要:类对象(
userClass
)字段名(
"name"
,"age"
)字段签名:
"Ljava/lang/String;"
:Java的String类型"I"
:Java的int类型
字段ID是后续访问字段的"钥匙"
3. 获取字段值
jstring name = (jstring)env->GetObjectField(user, nameField);
jint age = env->GetIntField(user, ageField);
GetObjectField
:获取对象类型字段的值(如String)需要对象实例和字段ID
返回
jobject
,需要转型为具体类型(如jstring
)
GetIntField
:获取基本类型int字段的值直接返回对应的基本类型(
jint
实际上是int
的别名)
4. 数据处理
std::string cppName = data_converter::convertJavaStringToCpp(env, name);
std::string processed = "User: " + cppName + ", Age: " + std::to_string(age);
将Java字符串转换为C++字符串(使用辅助工具类)
构造新的字符串信息,组合name和age
std::to_string
:将数字转换为字符串
5. 资源清理
env->DeleteLocalRef(name);
env->DeleteLocalRef(userClass);
DeleteLocalRef
:删除本地引用,防止内存泄漏JNI中的本地引用在函数返回后不会自动释放
虽然现代JVM通常能处理这些引用,但显式释放是良好实践
6. 返回结果
return env->NewStringUTF(processed.c_str());
NewStringUTF
:将C字符串(UTF-8)转换为Java字符串(jstring)这个返回的jstring会被自动管理,不需要手动释放
1. 基本类型映射
JNI定义了与Java基本类型对应的C/C++类型:
Java类型 | JNI类型 | C/C++类型 | 大小 |
---|---|---|---|
boolean | jboolean | unsigned char | 8位 |
byte | jbyte | signed char | 8位 |
char | jchar | unsigned short | 16位 |
short | jshort | short | 16位 |
int | jint | int | 32位 |
long | jlong | long long | 64位 |
float | jfloat | float | 32位 |
double | jdouble | double | 64位 |
2. 理解类型转换
字符串转换
Java字符串(jstring
)与C/C++字符串的转换是常见操作:
// Java String → C++ std::string
std::string jstringToStdString(JNIEnv* env, jstring jStr) {if (!jStr) return "";const char* cStr = env->GetStringUTFChars(jStr, nullptr);std::string cppStr(cStr);env->ReleaseStringUTFChars(jStr, cStr);return cppStr;
}// C++ std::string → Java String
jstring stdStringToJstring(JNIEnv* env, const std::string& cppStr) {return env->NewStringUTF(cppStr.c_str());
}
数组转换
处理Java数组更复杂一些:
// Java int[] → C++ std::vector<int>
std::vector<int> jintArrayToVector(JNIEnv* env, jintArray array) {std::vector<int> result;jsize length = env->GetArrayLength(array);if (length <= 0) return result;jint* elements = env->GetIntArrayElements(array, nullptr);result.assign(elements, elements + length);env->ReleaseIntArrayElements(array, elements, 0);return result;
}// C++ std::vector<int> → Java int[]
jintArray vectorToJintArray(JNIEnv* env, const std::vector<int>& vec) {jintArray result = env->NewIntArray(vec.size());env->SetIntArrayRegion(result, 0, vec.size(), vec.data());return result;
}
3. 内存管理和引用释放
引用类型
JNI有三种引用类型:
局部引用(Local Reference):
默认创建的引用
仅在当前native方法有效
方法返回后自动释放,但应及时手动释放
全局引用(Global Reference):
需要显式创建:
env->NewGlobalRef()
跨多个native调用有效
必须显式释放:
env->DeleteGlobalRef()
弱全局引用(Weak Global Reference):
类似全局引用但不阻止GC
创建:
env->NewWeakGlobalRef()
释放:
env->DeleteWeakGlobalRef()
最佳实践
JNIEXPORT void JNICALL Java_Example_memoryExample(JNIEnv* env, jobject obj) {// 1. 创建局部引用jclass localClass = env->FindClass("java/lang/String");// 2. 提升为全局引用jclass globalClass = (jclass)env->NewGlobalRef(localClass);// 3. 不再需要局部引用env->DeleteLocalRef(localClass);// ...使用globalClass...// 4. 最后释放全局引用env->DeleteGlobalRef(globalClass);
}
4. 高级主题
缓存字段ID和方法ID
// 在全局变量中缓存
struct CachedIDs {jclass exampleClass;jmethodID callbackMethod;jfieldID valueField;
};bool cacheIds(JNIEnv* env) {static CachedIDs cached;cached.exampleClass = (jclass)env->NewGlobalRef(env->FindClass("com/example/Example"));cached.callbackMethod = env->GetMethodID(cached.exampleClass, "callback", "(I)V");cached.valueField = env->GetFieldID(cached.exampleClass, "value", "I");return cached.exampleClass && cached.callbackMethod && cached.valueField;
}JNIEXPORT void JNICALL Java_Example_useCachedIds(JNIEnv* env, jobject obj) {static bool cached = cacheIds(env);if (!cached) return;// 使用缓存的IDsjint value = env->GetIntField(obj, cached.valueField);env->CallVoidMethod(obj, cached.callbackMethod, value + 1);
}
多线程注意事项
// 获取JVM指针以便在其他线程使用
JavaVM* g_jvm = nullptr;JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {g_jvm = vm;return JNI_VERSION_1_6;
}void backgroundThreadFunction() {JNIEnv* env;int status = g_jvm->AttachCurrentThread(&env, nullptr);if (status < 0) {// 处理错误return;}// 在这里可以安全使用JNI// ...g_jvm->DetachCurrentThread();
}
math_operations.h
#ifndef LEARN1_MATH_OPERATIONS_H
#define LEARN1_MATH_OPERATIONS_Hclass math_operations {
public:// 声明为静态成员函数(可通过类名直接调用)static int factorial(int n);static int fibonacci(int n);static bool isPrime(int num);
};#endif //LEARN1_MATH_OPERATIONS_H
1.头文件保护宏:
#ifndef LEARN1_MATH_OPERATIONS_H
#define LEARN1_MATH_OPERATIONS_H
// ...
#endif
这是防止头文件被多次包含的标准做法,避免重复定义错误。
2.类定义:
class math_operations {
public:// ...
};
定义了一个名为math_operations
的类,public:
表示后续成员都是公开的。
3.静态成员函数
static int factorial(int n);
static int fibonacci(int n);
static bool isPrime(int num);
这些是静态成员函数的声明,特点是:
static
关键字:表示这些函数是静态成员函数属于类而不是类的实例
可以直接通过类名调用,不需要创建对象实例
不能访问类的非静态成员
函数声明:
只提供了函数原型(返回类型、函数名、参数列表)
没有函数体实现(通常在对应的.cpp文件中实现)
为什么使用静态成员函数
工具类:当函数逻辑不依赖于对象状态时
命名空间替代:在C++中可以用来组织相关函数(替代命名空间)
无需实例化:可以直接调用,使用更方便
math_operations.cpp
#include "math_operations.h"int math_operations::factorial(int n) {if (n <= 1) return 1;return n * factorial(n - 1);
}int math_operations::fibonacci(int n) {if (n <= 1) return n;return fibonacci(n-1) + fibonacci(n-2);
}bool math_operations::isPrime(int num) {if (num <= 1) return false;for (int i = 2; i * i <= num; i++) {if (num % i == 0) return false;}return true;
}
#include "math_operations.h"
作用:包含对应的头文件,确保函数声明与定义一致
语法:使用引号
""
表示从当前目录或项目目录查找头文件
data_converter.h
#ifndef LEARN1_DATA_CONVERTER_H
#define LEARN1_DATA_CONVERTER_H#include <vector>
#include <string>
#include <jni.h> // 必须包含JNI头文件class data_converter {
public:static std::vector<int> convertJavaArrayToVector(JNIEnv* env, jintArray array);static std::string convertJavaStringToCpp(JNIEnv* env, jstring jStr);
};#endif //LEARN1_DATA_CONVERTER_H
<vector>
:提供C++标准向量容器支持<string>
:提供C++字符串支持<jni.h>
:JNI开发必需的头文件,定义了Java和本地代码交互的所有类型和函数JNIEnv* env
:JNI环境指针,提供所有JNI函数访问
data_converter.cpp
#include "data_converter.h"
#include <vector>std::vector<int> data_converter::convertJavaArrayToVector(JNIEnv* env, jintArray array) {std::vector<int> result;jsize length = env->GetArrayLength(array);jint* elements = env->GetIntArrayElements(array, nullptr);result.assign(elements, elements + length);env->ReleaseIntArrayElements(array, elements, 0);return result;
}std::string data_converter::convertJavaStringToCpp(JNIEnv* env, jstring jStr) {const char* cStr = env->GetStringUTFChars(jStr, nullptr);std::string result(cStr);env->ReleaseStringUTFChars(jStr, cStr);return result;
}
1. convertJavaArrayToVector
方法
功能
将 Java 的 int[]
数组转换为 C++ 的 std::vector<int>
实现步骤:
创建空 vector:
std::vector<int> result
获取数组长度:
env->GetArrayLength(array)
获取 Java 数组的长度jsize
是 JNI 中表示大小的类型
获取数组元素指针:
env->GetIntArrayElements(array, nullptr)
获取指向 Java 数组内容的指针第二个参数
nullptr
表示不关心是否复制了数组
填充 vector:
result.assign(elements, elements + length)
将 Java 数组内容复制到 vector
释放资源:
env->ReleaseIntArrayElements(array, elements, 0)
释放获取的数组指针参数
0
表示将内容复制回原数组并释放临时内存
返回结果:包含 Java 数组数据的 vector
关键点:
必须成对调用
GetIntArrayElements
和ReleaseIntArrayElements
assign
方法高效地将 C 风格数组复制到 vector最后一个参数
0
可以是:0
:复制回原数组并释放临时内存JNI_ABORT
:不复制回原数组但释放内存JNI_COMMIT
:复制回原数组但不释放内存
2. convertJavaStringToCpp
方法
功能
将 Java 的 String
对象转换为 C++ 的 std::string
实现步骤:
获取 UTF-8 字符指针:
env->GetStringUTFChars(jStr, nullptr)
获取指向 Java 字符串 UTF-8 编码的指针第二个参数
nullptr
表示不关心是否复制了字符串
创建 std::string:
std::string result(cStr)
用获取的字符指针构造 C++ 字符串
释放资源:
env->ReleaseStringUTFChars(jStr, cStr)
释放获取的字符指针
返回结果:包含 Java 字符串内容的 std::string
关键点:
必须成对调用
GetStringUTFChars
和ReleaseStringUTFChars
使用 UTF-8 编码转换,这是 Java 和 C++ 之间最常用的编码方式
如果 Java 字符串包含非 ASCII 字符,这种方式能正确处理
3. 可能的改进
添加空指针检查:
if (!jStr) return std::string();
错误处理增强:
const char* cStr = env->GetStringUTFChars(jStr, nullptr); if (!cStr) {// 处理错误 }
string_processor.h
#ifndef LEARN1_STRING_PROCESSOR_H
#define LEARN1_STRING_PROCESSOR_H#include <string>class string_processor {
public:// 1. 字符串反转static std::string reverseString(const std::string& input);// 2. 统计元音字母数量static int countVowels(const std::string& input);// 3. 凯撒加密(字母位移)static std::string encryptString(const std::string& input, int shift);
};#endif //LEARN1_STRING_PROCESSOR_H
const std::string& input
:要反转的字符串(常量引用,避免拷贝)
const std::string&
讲解
1. 基本组成解析
可以分解为三个部分:
std::string
- C++标准库中的字符串类型&
- 表示这是一个引用const
- 表示这是一个常量(不可修改)
2. 为什么要使用这种形式
2.1 避免不必要的拷贝
不使用引用:
void func(std::string str)
传递参数时会创建字符串的完整副本(拷贝构造)使用引用:
void func(const std::string& str)
只传递引用(内存地址),不创建副本
2.2 保证原字符串不被修改
const
限定确保函数内不能修改原字符串既享受引用的高效,又保证数据安全
2.3 支持临时对象
可以接受临时字符串对象(右值)
例如:
func("temporary string")
3. 与替代方案的对比
参数形式 | 拷贝开销 | 可修改原值 | 接受临时对象 | 备注 |
---|---|---|---|---|
std::string | 有 | 副本可修改 | 是 | 最安全但效率最低 |
std::string& | 无 | 可以修改 | 否 | 需要非常量左值 |
const std::string& | 无 | 不可修改 | 是 | 最佳平衡方案 |
std::string_view (C++17) | 无 | 不可修改 | 是 | 现代替代方案 |
4. 典型使用场景
4.1 作为输入参数
void printString(const std::string& str) {std::cout << str; // 只能读取,不能修改
}
4.2 与STL算法配合
bool contains(const std::string& str, const std::string& substr) {return str.find(substr) != std::string::npos;
}
4.3 类成员函数
class TextProcessor {
public:void process(const std::string& input) {// 处理输入但不修改它}
};
5. 注意事项
生命周期管理:
引用必须确保指向的对象在函数使用期间有效
不要返回局部变量的const引用
C++17后的替代方案:
考虑使用
std::string_view
作为只读参数更轻量级,支持更多类型的字符串数据
与移动语义的关系:
对于需要"夺取"所有权的情况,应使用
std::string&&
(右值引用)const&
会阻止移动语义的应用
NULL问题:
不能直接传递NULL/nullptr
如果需要可空引用,应使用指针或
std::optional
string_processor.cpp
#include "string_processor.h"
#include <algorithm> // 提供std::reverse
#include <string> // 提供std::stringstd::string string_processor::reverseString(const std::string& input) {std::string reversed = input;std::reverse(reversed.begin(), reversed.end());return reversed;
}int string_processor::countVowels(const std::string& input) {int count = 0;const std::string vowels = "aeiouAEIOU";for (char c : input) {if (vowels.find(c) != std::string::npos) {count++;}}return count;
}std::string string_processor::encryptString(const std::string& input, int shift) {std::string result;for (char c : input) {if (isalpha(c)) {char base = isupper(c) ? 'A' : 'a';c = ((c - base + shift) % 26) + base;}result += c;}return result;
}