静态加载指的是在java类加载时自动加载本地库,在同一个进程中对同一个库名只会加载一次。有以下特点:
public class NativeHelper {
// 静态加载方式
static {
try {
System.loadLibrary("native-lib");
} catch (UnsatisfiedLinkError e) {
Log.e("JNI", "加载本地库失败: " + e.getMessage());
}
}
public native String stringFromJNI();
}
动态加载指的是在运行时根据需要手动加载本地库。有以下特点:、
public class NativeHelper {
private boolean isLibLoaded = false;
public void loadLibrary(String fullPath) {
if (!isLibLoaded) {
System.load(fullPath); // 动态加载,如 "/data/data/com.example/app_lib/libnative-lib.so"
isLibLoaded = true;
}
}
public native String stringFromJNI();
}
通过函数名自动关联Java native方法和本地实现,依赖固定的函数命名规则(Java_包名_类名_方法名,类名中的特殊字符(如 $)需转义),只在首次调用native方法时查找符号。
// Native 层(无需显式注册)
JNIEXPORT void JNICALL
Java_com_example_NativeHelper_helloFromJNI(JNIEnv *env, jobject thiz) {
// 实现代码
}
加载库后立即调用JNI_OnLoad主动注册本地方法(RegisterNatives),在方法调用前就完成所有注册,更加灵活高效。不强制要求注册所有函数,即使实现了JNI_OnLoad,未注册的函数仍可通过静态注册规则被调用。
以下是使用JNI_OnLoad注册的一个简单的示例:
#include
// 本地方法实现
jstring native_hello(JNIEnv *env, jobject thiz) {
return (*env)->NewStringUTF(env, "Hello from dynamic registration!");
}
// 方法映射表
static JNINativeMethod methods[] = {
{"helloFromJNI", "()Ljava/lang/String;", (void *)native_hello}
};
// JNI_OnLoad 实现
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
// 1. 获取JNI环境指针
if (vm->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
// 2. 查找目标Java类
jclass clazz = env->FindClass("com/example/NativeHelper");
if (clazz == NULL) {
return JNI_ERR;
}
// 3. 注册本地方法
if (env->RegisterNatives(clazz, gMethods,
sizeof(gMethods)/sizeof(gMethods[0])) < 0) {
return JNI_ERR;
}
// 4. 返回使用的JNI版本
return JNI_VERSION_1_6;
}
对代码中内容做一点解释:
typedef struct {
const char* name; // Java中的方法名
const char* signature; // 方法签名
void* fnPtr; // 本地函数指针
} JNINativeMethod;
方法签名的基本格式为:“(参数类型)返回类型”。参数类型分为三种:基本数据类型、引用数据类型,数组类型,分别有不同的签名方式。
多个参数直接拼接,如"(IJLjava/lang/String;)V"表示"void (int, long, String)"。
Java类型 | JNI签名 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
Java类型 | JNI签名 |
---|---|
String | Ljava/lang/String; |
Object | Ljava/lang/Object; |
Class> | Ljava/lang/Class; |
Throwable | Ljava/lang/Throwable; |
引用类型必须用"L"开头,并用";“结尾,如"Ljava/lang/String;”。
如果是内部类,需要用$,比如"Landroid/media/MediaCodec$CryptoInfo;"
Java类型 | JNI签名 |
---|---|
int[] | [I |
String[] | [Ljava/lang/String; |
int[][] | [[I |
Object[] | [Ljava/lang/Object; |
数组类型用"[“开头,如”[I"表示"int[]"。
本地方法的前两个参数具有固定的含义,它们由JVM自动传递,用于提供环境上下文和调用对象信息。
实例方法:
JNIEXPORT 返回类型 JNICALL
Java_包名_类名_方法名(JNIEnv *env, jobject thiz, ...) {
// 实现代码
}
静态方法:
JNIEXPORT 返回类型 JNICALL
Java_包名_类名_方法名(JNIEnv *env, jclass clazz, ...) {
// 实现代码
}
JNI数据类型映射
Java类型 | JNI类型 | 说明 |
---|---|---|
boolean | jboolean | 无符号8位 |
byte | jbyte | 有符号8位 |
char | jchar | 无符号16位(UTF-16) |
short | jshort | 有符号16位 |
int | jint | 有符号32位 |
long | jlong | 有符号64位 |
float | jfloat | 32位浮点 |
double | jdouble | 64位浮点 |
void | void | 无返回值 |
int[] | jintArray | 数组类型,其他基本类型数组类似 |
String | jstring | 字符串 |
Object | jobject | 任意Java对象(包含自定义) |
String[] | jobjectArray | 字符串数组 |
Object[] | jobjectArray | 任意Java对象数组 |
JNI的基础类型与C/C++原生类型无缝对接,可以像使用普通变量一样操作它们。只有在处理Java对象、字符串、数组等引用类型时,才需要调用JNIEnv提供的方法。
以下方法用于类的查找、对象的创建以及方法和字段的访问
用于Java字符串与本地字符串的转换:
从本地字符串创建Java字符串,返回的是java堆上的对象(jstring),由JVM的垃圾回收机制管理,本地代码只需将jstring返回给Java层或传递给其他JNI方法,无需额外释放。
从Java字符串获取本地字符串,用完jstring之后必须配对调用ReleaseStringUTFChars,防止本地内存泄漏(当 JNI 复制字符串内容时),阻止 Java 字符串被 GC 回收(当 JNI 持有内部引用时)。必须复制通过 GetStringUTFChars获取的字符串才能在JNI资源释放后继续使用。
这些方法用于数组的创建、访问和修改:
用于管理对象引用:
在JNI中,绝大多数JNI函数创建的都是局部引用,只有NewGlobalRef和NewWeakGlobalRef会创建全局引用。
以下示例是局部引用:
jstring str = (*env)->NewStringUTF(env, "Hello"); // 创建局部引用
jobject obj = (*env)->NewObject(env, cls, ctor); // 创建局部引用
jobject arr = (*env)->GetObjectField(env, obj, fieldID); // 创建局部引用
一般来说局部引用会自动释放,如果重复使用某个变量,可以手动调用DeleteLocalRef。
要注意的是FindClass返回的jclass是局部引用,如果要长期使用需要转为全局引用:
static jclass stringClass; // 全局变量
jclass localCls = env->FindClass("java/lang/String"); // 局部引用
stringClass = env->NewGlobalRef(localCls); // 转为全局引用
jmethodID和jfieldID不是对象引用,而是方法/字段的标识符,无需创建全局引用!
以MediaCodec为例:frameworks/base/media/jni/android_media_MediaCodec.cpp
创建JMediaCodec时,传入了jobject,将它设置为全局弱引用,
mObject = env->NewWeakGlobalRef(thiz);
之后直接获取JNIEnv,调用mObject的回调方法postEventFromNative
JNIEnv *env = AndroidRuntime::getJNIEnv();
env->CallVoidMethod(
mObject, gFields.postEventFromNativeID,
EVENT_FIRST_TUNNEL_FRAME_READY, arg1, arg2, obj);
将JMediaCodec存储到java对象的字段中:
env->CallVoidMethod(thiz, gFields.setAndUnlockContextID, (jlong)codec.get());
我们实际在使用弱引用之前要使用以下两个方法判断是否被回收
if (env->IsSameObject(weakGlobalRef, NULL)) {
// 弱全局引用已被回收
} else {
// 弱全局引用仍有效
}
jobject liveObj = (*env)->NewLocalRef(env, weakGlobalRef);
if (liveObj == NULL) {
// 对象已被回收
}