NDK笔记(关于Android中Jni的动态注册)
题记
Android app加载.c/cpp和.so/.a就必然要谈到jni接口的编写,jni接口注册有俩种方式:动态和静态注册。静态注册的方式固然方便快捷,但是这样的话简单demo可以,为了项目的工程化,还是有必要引入动态注册的,好处会在下面讲(恩,我已经说服了我自己,目前的工作中已经逐步替换为动态注册了)。下面就以一个Kotlin工程为例,逐步梳理下详细过程。
建议配合Java/Kotlin与Native层的相互调用一起看,俩片结合下来(代码归档在github同一个工程)JNI基础知识可以了解个七七八八。建议收藏哈~
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")
执行的时候就会把方法注册,所以说,提前加载,减少运行时间。我们要做的就是重写他。
- 通过
jint GetEnv(void** env, jint version)
创建一个JavaVm。第一个参数为创建的指针变量,第二个参数为JNI的NDK版本,非JAVA版本。这个是我们动态注册的关键,同时多线程也会用到它(后续补充)。所以我们升级JavaVm为全局变量通过此方法写入指针,得到指针变量。 - 注意这个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基础类型传递,反射,方法调用整理一下,方便自己复制粘贴,加油~