MainActivity.kt
package com.demo.learn1
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
class MainActivity : ComponentActivity() {
// 加载原生库
init {
System.loadLibrary("native_code")
}
// 声明原生方法
// 数学运算
private external fun computeFactorial(n: Int): Int
private external fun computeFibonacci(n: Int): Int
// 字符串处理
private external fun reverseString(input: String): String
private 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): String
override 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.cpp
math_operations.cpp
string_processor.cpp
data_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);
// 获取字段ID
jfieldID 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;
// 使用缓存的IDs
jint 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_H
class 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::string
std::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;
}