Android JNI中巧妙的使用动态注册

NDK笔记(关于Android中Jni的动态注册)


Android app加载.c/cpp和.so/.a就必然要谈到jni接口的编写,jni接口注册有俩种方式:动态和静态注册。静态注册的方式固然方便快捷,但是这样的话简单demo可以,为了项目的工程化,还是有必要引入动态注册的,好处会在下面讲(恩,我已经说服了我自己,目前的工作中已经逐步替换为动态注册了)。下面就以一个Kotlin工程为例,逐步梳理下详细过程。

1.静态注册

新建工程创建一个Jni接口的class工具类。

class JniUtils {
    external fun stringFromJNI(): String
    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }
}

这个在工程中external这一行会爆红,鼠标放上去,alt+enter点击creat jni…编译器就会替你生成一个jni接口,这个冗长的函数就是一个静态注册的jni接口函数。同时编译器的error也会消失。

extern "C"
JNIEXPORT jstring JNICALL
Java_com_heima_jnitest_JniUtils_stringFromJNI(JNIEnv *env, jobject thiz) {
    // TODO: implement stringFromJNI()
}

2.动态注册

相比于静态注册,动态注册就有以下安全点:

  • 被反编译后安全性高(用着放心)
  • native中函数名简洁(看着舒服)
  • 编译后的函数标记较短一些(调用方便)
    我们只有关注2个大点:JNI_OnLoad()jniNativeMethod

2.1 寻找方法签名

关于动态注册那个冗长的方法,在动态注册时候我们需要把它变短,怎么变短呢?这就牵扯到一个结构体jniNativeMethod,需要用到类跟方法的签名。我们找到生成的class的文件夹,通过命令javap来找到签名,当然也可以用javah,道理都是一样的。
控制台操作
详细方法入上图所示,找到class→javap→得到签名。得到如下标识:

警告: 二进制文件Jniutils包含com.heima.jnitest.JniUtils
Compiled from "JniUtils.kt"
public final class com.heima.jnitest.JniUtils {
  public static final com.heima.jnitest.JniUtils$Companion Companion;
    descriptor: Lcom/heima/jnitest/JniUtils$Companion;
  public final native java.lang.String stringFromJNI();
    descriptor: ()Ljava/lang/String;

  public com.heima.jnitest.JniUtils();
    descriptor: ()V

  static {};
    descriptor: ()V
}

这个签名有什么用呢?这就牵扯到动态注册中很重要的结构体JNINativeMethod 其中的signature就是我们通过命令行输出的descriptor,关于网上所说的什么签名对照表什么的,我是记不住,每次敲敲命令行就好,真的没必要查表。

jstring stringFromJNI(JNIEnv *env, jobject thiz) {//冗长方法直接删短
    // TODO: implement stringFromJNI()
}
/*
 1. typedef struct {
    const char* name; //函数名字
    const char* signature; //函数符号
    void*       fnPtr; //函数指针
    } JNINativeMethod;
 */
static const JNINativeMethod jniNativeMethod[] = {
        {"stringFromJNI", "(Ljava/lang/String;)V", (void *) (stringFromJNI)},
};

2.2 JNI_OnLoad

说到动态注册就要说道JNI_OnLoad这个方法,这个方法会在System.loadLibrary("native-lib")执行的时候就会把方法注册,所以说,提前加载,减少运行时间。我们要做的就是重写他。

  1. 通过jint GetEnv(void** env, jint version)创建一个JavaVm。第一个参数为创建的指针变量,第二个参数为JNI的NDK版本,非JAVA版本。这个是我们动态注册的关键,同时多线程也会用到它(后续补充)。所以我们升级JavaVm为全局变量通过此方法写入指针,得到指针变量。
  2. 注意这个jint返回值。点到jni.h里面会有详细的解释。
    #define JNI_FALSE   0
    #define JNI_TRUE    1
    //Jni版本
    #define JNI_VERSION_1_1 0x00010001
    #define JNI_VERSION_1_2 0x00010002
    #define JNI_VERSION_1_4 0x00010004
    #define JNI_VERSION_1_6 0x00010006
    //返回值类型
    #define JNI_OK          (0)         /* no error */
    #define JNI_ERR         (-1)        /* generic error */
    #define JNI_EDETACHED   (-2)        /* thread detached from the VM */
    #define JNI_EVERSION    (-3)        /* JNI version error */
    #define JNI_ENOMEM      (-4)        /* Out of memory */
    #define JNI_EEXIST      (-5)        /* VM already created */
    #define JNI_EINVAL      (-6)        /* Invalid argument */
    
    #define JNI_COMMIT      1           /* copy content, do not free buffer */
    #define JNI_ABORT       2           /* free buffer w/o copying back */
    

3.使用jclass FindClass(const char* name)函数通过反射获取到jclass对象
4. 使用jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)动态注册jni函数。

  • 第一个参数为上一步获取的jclass;
  • 第二个参数为Jni方法体,也就是我们第一步通过方法签名编写的JNINativeMethod集合;
  • 第三个参数为JNINativeMethod的长度,也就是要动态加载的函数的数量

直接贴代码,一切尽在注释中。

/**
 * 1.设置jvm全局变量,多线程需要用到
 * 2.nullptr: C++11后,要取代NULL,作用是可以给初始化的指针赋值
 */
JavaVM *jvm = nullptr;
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *javaVm, void *pVoid) {
    jvm = javaVm;
    // 1.通过JavaVM 创建全新的JNIEnv
    JNIEnv *jniEnv = nullptr;
    // 2.判断创建是否成功
    jint result = javaVm->GetEnv(reinterpret_cast<void **>(&jniEnv),JNI_VERSION_1_6); // 参数2:是JNI的版本 NDK 1.6   JavaJni 1.8
    if (result != JNI_OK) {
        return -1; // 主动报错
    }
    // 3.找到需要动态动态注册的Jni类
    jclass jniClass = jniEnv->FindClass("com/heima/jnitest/JniUtils");
    //动态注册(这里就需要用到签名后的方法了)   待注册class 方法集合 方法数量
    jniEnv->RegisterNatives(jniClass, jniNativeMethod,sizeof(jniNativeMethod) / sizeof(JNINativeMethod));
    return JNI_VERSION_1_6;
}

大功告成,这是个最简单的动态JNI接口的加载。最后附上代码下载地址

趁着拔智齿在家休息,梳理一遍,准备把JNI基础类型传递,反射,方法调用整理一下,方便自己复制粘贴,加油~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章