Android JNI开发

发布于:2025-05-31 ⋅ 阅读:(23) ⋅ 点赞:(0)

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) {
    // 对象已被回收
}