Android JNI开发
1、Android JNI 动态库加载方式
1.1、静态加载
静态加载指的是在java类加载时自动加载本地库,在同一个进程中对同一个库名只会加载一次。有以下特点:
- 使用System.loadLibrary(),库必须位于APK的jniLibs目录或系统库路径中
- 在类的静态初始化块中加载,加载时机是在类初始化时
- 只需指定库名称(不含前缀lib和后缀.so)
public class NativeHelper {// 静态加载方式static {try {System.loadLibrary("native-lib");} catch (UnsatisfiedLinkError e) {Log.e("JNI", "加载本地库失败: " + e.getMessage());}}public native String stringFromJNI();
}
1.2、动态加载
动态加载指的是在运行时根据需要手动加载本地库。有以下特点:、
- 使用System.load()方法
- 需要指定库的完整路径
- 可以在任何时间点加载
- 更适合插件化或动态功能模块的场景
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();
}
2、JNI函数的两种注册方式
2.1、静态注册(固定命名规则)
通过函数名自动关联Java native方法和本地实现,依赖固定的函数命名规则(Java_包名_类名_方法名,类名中的特殊字符(如 $)需转义),只在首次调用native方法时查找符号。
// Native 层(无需显式注册)
JNIEXPORT void JNICALL
Java_com_example_NativeHelper_helloFromJNI(JNIEnv *env, jobject thiz) {// 实现代码
}
2.2、动态注册(JNI_OnLoad)
加载库后立即调用JNI_OnLoad主动注册本地方法(RegisterNatives),在方法调用前就完成所有注册,更加灵活高效。不强制要求注册所有函数,即使实现了JNI_OnLoad,未注册的函数仍可通过静态注册规则被调用。
以下是使用JNI_OnLoad注册的一个简单的示例:
#include <jni.h>// 本地方法实现
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;
}
对代码中内容做一点解释:
- JavaVM 指针:
- JavaVM是JNI提供的虚拟机接口,全局唯一,在整个进程生命周期内有效;
- 由系统在调用JNI_OnLoad时传入,用于获取当前线程的JNIEnv
- JNIEnv:
- JNIEnv是一个指向JNI函数表的指针,包含了所有JNI接口函数(如FindClass、NewStringUTF等)。因此,在使用任何JNI功能前(如注册 Native 方法、查找类),必须先获取JNIEnv
- JNIEnv是线程局部的,不同线程的JNIEnv不同,因此必须为当前线程获取该指针
- JNINativeMethod是一个结构体:
typedef struct {const char* name; // Java中的方法名const char* signature; // 方法签名void* fnPtr; // 本地函数指针
} JNINativeMethod;
2.3、JNINativeMethod中的方法签名
方法签名的基本格式为:“(参数类型)返回类型”。参数类型分为三种:基本数据类型、引用数据类型,数组类型,分别有不同的签名方式。
多个参数直接拼接,如"(IJLjava/lang/String;)V"表示"void (int, long, String)"。
2.3.1、基本数据类型签名
Java类型 | JNI签名 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
2.3.2、引用数据类型签名
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;"
2.3.3、数组类型签名
Java类型 | JNI签名 |
---|---|
int[] | [I |
String[] | [Ljava/lang/String; |
int[][] | [[I |
Object[] | [Ljava/lang/Object; |
数组类型用"[“开头,如”[I"表示"int[]"。
2.4、JNI函数
本地方法的前两个参数具有固定的含义,它们由JVM自动传递,用于提供环境上下文和调用对象信息。
实例方法:
JNIEXPORT 返回类型 JNICALL
Java_包名_类名_方法名(JNIEnv *env, jobject thiz, ...) {// 实现代码
}
- 第一个参数(JNIEnv *env):JNI 环境指针,用于调用 NI函数(如创建Java对象、调用Java方法等)。每个线程有独立的JNIEnv,不可跨线程传递。
- 第二个参数(jobject thiz):对Java对象的引用,表示调用该方法的实例(相当于 Java中的this)。
静态方法:
JNIEXPORT 返回类型 JNICALL
Java_包名_类名_方法名(JNIEnv *env, jclass clazz, ...) {// 实现代码
}
- 第一个参数(JNIEnv *env):JNI环境指针。
- 第二个参数(jclass clazz):对Java类的引用,表示调用该静态方法的类(相当于 Java中的Class对象)。
3、常见的JNIEnv方法
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提供的方法。
3.1、类和对象操作方法
以下方法用于类的查找、对象的创建以及方法和字段的访问
- jclass FindClass(const char* name):能够查找Java类。
- jclass GetObjectClass(jobject obj):可获取对象的类。
- jmethodID GetMethodID(jclass clazz, const char* name, const char* sig):能获取方法ID。
- void CallVoidMethod(jobject obj, jmethodID methodID, …):用于调用返回void类型的实例方法,此外根据返回值类型还有 CallIntMethod、CallObjectMethod等变体。
- jmethodID GetStaticMethodID(jclass clazz, const char* name, const char* sig):可获取静态方法 ID。
- void CallStaticVoidMethod(jclass clazz, jmethodID methodID, …):用于调用静态方法,此外根据返回值类型还有CallStaticIntMethod、CallSTaticObjectMethod等变体。
- jfieldID GetFieldID(jclass clazz, const char* name, const char* sig):能获取字段ID。
- jint GetIntField(jobject obj, jfieldID fieldID):可获取 int 类型的实例字段,另外根据字段类型有不同变体GetObjectField、GetLongField 等。
- jobject NewObject(jclass clazz, jmethodID methodID, …):用于创建新对象。
- void SetIntField(jobject obj, jfieldID fieldID, jint value):设置实例字段值,有SetObjectField等变体
3.2、字符串操作方法
用于Java字符串与本地字符串的转换:
- jstring NewStringUTF(const char* bytes):能创建 Java 字符串。
- const char * GetStringUTFChars(jstring string, jboolean* isCopy):可获取字符串的 UTF-8 编码形式。
- void ReleaseStringUTFChars(jstring string, const char* utf):用于释放字符串资源。
从本地字符串创建Java字符串,返回的是java堆上的对象(jstring),由JVM的垃圾回收机制管理,本地代码只需将jstring返回给Java层或传递给其他JNI方法,无需额外释放。
从Java字符串获取本地字符串,用完jstring之后必须配对调用ReleaseStringUTFChars,防止本地内存泄漏(当 JNI 复制字符串内容时),阻止 Java 字符串被 GC 回收(当 JNI 持有内部引用时)。必须复制通过 GetStringUTFChars获取的字符串才能在JNI资源释放后继续使用。
3.3、数组操作方法
这些方法用于数组的创建、访问和修改:
- jintArray NewIntArray(jint length):可创建 int 类型的数组,此外还有 NewByteArray、NewObjectArray 等变体。
- void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value); 根据设置类型有不同变体
- jint * GetIntArrayElements(jintArray array, jboolean* isCopy):能获取数组元素。
- void ReleaseIntArrayElements(jintArray array, jint* elems, jint mode):用于释放数组资源。
- jsize GetArrayLength(jarray array):可获取数组长度。
3.4、全局引用和局部引用操作方法
用于管理对象引用:
- jobject NewGlobalRef(jobject obj):可创建全局引用。
- void DeleteGlobalRef(jobject obj):用于删除全局引用。
- jobject NewLocalRef(jobject obj):能创建局部引用。
- void DeleteLocalRef(jobject obj):用于删除局部引用。
- jobject NewWeakGlobalRef(jobject obj):创建全局弱引用
- void DeleteWeakGlobalRef(jobject obj):删除全局弱引用
在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) {// 对象已被回收
}