JNI——动态注册

124 阅读2分钟

前言

前面我们都是通过编译器生成的 JNI 函数,属于静态注册,静态注册比动态注册简单,但是在 Android 系统源码中会发现大量使用动态注册,比如你打开 /frameworks/base/core/jni/android_os_Parcel.cpp 的源码,会发现里面都是使用的动态注册,动态注册不需要暴露包名,安全性更高。

原理

在使用 System.loadLibrary(String libname) 加载 so 库时,会调用 JNI_OnLoad(JavaVM* vm, void* reserved) 函数,我们可以通过重写该函数来实现动态注册。重写后该函数的左边会出现 image.png 标志, 点击该标志可以跳转到 jni.h 中对应的函数位置。

动态注册需要使用 JNIEnv* 中的 RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods) 函数,其中 JNIEnv* 可以通过 JavaVM* 获取到,参数 JNINativeMethod 是一个结构体,其代码如下:

typedef struct {
    const char* name;       // Java 函数名 
    const char* signature;  // 签名
    void*       fnPtr;      // 函数指针
} JNINativeMethod;

该结构体由 3 部分组成:Java 函数名、签名和函数指针,因此我们只需要拿到 JNIEnv*、 Java 函数名、签名和函数指针,并在 JNI_OnLoad() 函数中调用 RegisterNatives() 函数即可实现动态注册。

具体实现

下面我们用代码来实现动态注册,MainActivity 代码如下:

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("jnitest");
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        dynamicRegTest1();
        dynamicRegTest2("Raymond");
    }

    private native void dynamicRegTest1();

    private native int dynamicRegTest2(String name);
}

MainActivity 中有两个 natvie 函数,可以看到这里跟静态注册的方式没区别,C++ 的代码就不一样了,native-lib.cpp 中的代码如下:

#include <jni.h>
#include <string>

// 日志输出
#include <android/log.h>

#define TAG "jni"

// __VA_ARGS__ 代表可变参数
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG,  __VA_ARGS__);

// 该函数的两个参数没有用到可以不写
void dynamicMethod1(JNIEnv* env, jobject thiz){
    LOGD("Dynamic register method")
}

int dynamicMethod2(JNIEnv* env, jobject thiz, jstring str){
    const char* msg = env->GetStringUTFChars(str, nullptr);
    LOGD("His name is %s", msg)
    env->ReleaseStringUTFChars(str, msg);
    return 0;
}

JNINativeMethod jniNativeMethods[] = {
        {"dynamicRegTest1", "()V", (void *)(dynamicMethod1)},
        {"dynamicRegTest2", "(Ljava/lang/String;)I", (void *)(dynamicMethod2)}
};

jint JNI_OnLoad(JavaVM* vm, void* args){
    JNIEnv* env = nullptr;
    // 给 env 赋值
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return JNI_ERR;
    }

    jclass jclazz = env->FindClass("com/example/jnitest/MainActivity");
    if (jclazz == nullptr) {
        LOGD("MainActivity class not found!");
        return JNI_ERR;
    }
    
    // 注册 Native 函数
    if (env->RegisterNatives(jclazz, jniNativeMethods, sizeof(jniNativeMethods)/sizeof(JNINativeMethod)) < 0) {
        LOGD("Failed to register natives");
        return JNI_ERR;
    }

    // 返回最新的 JNI 版本号
    return JNI_VERSION_1_6;
}

这里首先要把 JNINativeMethod 准备好,然后在 JNI_OnLoad() 中是通过 vm->GetEnv() 给 env 赋值,通过 env 调用 RegisterNatives() 函数,该函数代码如下:

jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)
{ return functions->RegisterNatives(this, clazz, methods, nMethods); }

需要传入 3 个参数,依次是:Java 函数所属的类、JNINativeMethod 和 函数个数。

运行后打印如下:

Dynamic register method
His name is Raymond

可以看到成功地通过动态注册实现了 Java 函数对 Native 函数的调用。