什么是JNI
JNI指的是Java Native Interface,即JAVA原生接口,它的作用是定义了JAVA代码与C/C++的代码进行互动的方式,从而使得Java能够调用c/c++代码,也可以使得c/c++能够调用java的方法(回调),方便原生开发,严格来说,其实是使用 Java 或 Kotlin 编程语言编写的字节码与原生代码的互动,所以Kotlin也是完全支持的
JNI在Android中的使用方式
配置c/c++编译环境
由于本文并不是主要说明如何配置Native的开发环境,且官网说明也比较详细,故配置过程可以参考官网的说明进行https://developer.android.google.cn/studio/projects/add-native-code,这边不做另外说明
注意,本文会采用CMake的编译方式进行接下来的说明
加载
JAVA层需要通过System.loadLibrary方法来加载so库,通过在静态初始化方法中调用 JAVA调用方式
JAVA
public class MainNativeInterface {
static {
System.loadLibrary("native-lib");
}
}
Kotlin
class MainNativeInterfaceKotlin {
companion object {
init {
System.loadLibrary("native-lib")
}
}
}
注意这边的Kotlin写法上,我们使用了companion object里面来执行init,这样最终翻译出来的Java代码会是static静态代码块,如果init放在外层,则Java代码会变成在该类的构造方法中调用。官方推荐做法也是在静态初始化方法中调用,这样可以提前加载
注册Native方法
一般的说法上,我们会把Native方法区分成为静态的注册方式和动态的注册方式2种,但是无论是静态的注册还是动态的注册方式,Native的方法都是在运行时进行注册绑定的,静态注册的方式JNI层会在方法调用的时候去查找符合相应规则的方法名进行绑定,而动态注册则会在JNI进行加载动态库的时候就完成所有方法的绑定,所以其实可以理解为静态注册是隐式的被动注册方式,而动态注册则是显示的主动注册方式,下面我们来看一下这两种方式的具体做法,就能发现其显示、隐式,主动、被动的区别
静态注册-隐式被动
该方式通过JNI替我们完成注册的过程,我们只需要按一定的方法名称规则配置好我们的方法名即可,先来看一下Java层的写法
JAVA
public class MainNativeInterface {
static {
System.loadLibrary("native-lib");
}
public native static String stringFromJNI();
}
Kotlin
class MainNativeInterface {
companion object {
init {
System.loadLibrary("native-lib")
}
@JvmStatic
external fun stringFromJNI(): String
}
}
我们定义了一个stringFromJNI方法,该方法为native的方法,并且这边是一个static的方法
注:Kotlin版本为了对应Java的版本,我们使用了JvmStatic注解来表明其是一个static的方法,并且其是直接属于MainNativeInterface这个类的,这边可能会有同学疑惑,为什么companion object不行,大家可以尝试去除它,然后看其翻译的Java版本的代码就一目了然了(Tools->Kotlin->showKotlinBytecode, Decompile),原来Kotlin里面的companion object会创建一个静态的Companion类,然后将该方法作为该静态类的内部方法,这样做无疑对于JNI在进行方法查询的绑定的时候会产生影响,我们在写JNI层方法名称的时候,只会写包名+类名+方法名,而类名我们只会写MainNativeInterface,所以会导致绑定失败 java.lang.NoSuchMethodError: no static or non-static method
然后我们在cpp目录下有个native-lib.cpp文件,在里面我们定义对应的native层的方法
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_nativeevaldemo_MainNativeInterface_stringFromJNI(JNIEnv *env, jclass thiz) {
return env->NewStringUTF("native auto search method bind");
}
这样,我们就完成了方法的隐式被动注册,使用起来也十分方便,因为我们声明了static的方法,所以直接调用该静态native方法即可,当然你也可以使用非静态的方法
JAVA
public class MainNativeInterface {
static {
System.loadLibrary("native-lib");
}
public native String stringFromJNI();
}
Kotlin
class MainNativeInterface {
companion object {
init {
System.loadLibrary("native-lib")
}
}
external fun stringFromJNI(): String
}
JNI层需要修改该方法传递进来的最后一个参数为jobject
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_nativeevaldemo_MainNativeInterface_stringFromJNI(JNIEnv *env, jobject thiz) {
return env->NewStringUTF("native auto search method bind");
}
在使用上,非静态方法就会有点费劲了,需要先new出来这个class再进行使用咯
显式主动注册
该方式需要我们自己去主动向JVM注册绑定Native方法 Java层我们不需要变,我们来看一下JNI层的写法,在native-lib.cpp下创建一个JNI_OnLoad方法
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass c = env->FindClass("com/example/nativeevaldemo/MainNativeInterface");
if (c == nullptr) return JNI_ERR;
static const JNINativeMethod methods[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void*) stringFromJNI}
};
int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod));
if (rc != JNI_OK) return rc;
return JNI_VERSION_1_6;
}
static jstring JNICALL
stringFromJNI(JNIEnv *jenv, jclass ths){
return jenv->NewStringUTF("native hand method bind");
}
这边需要注意的是
- FindClass里面,我们需要使用"/"来代替“.”,来指定我们的class完整的包路径
- 绑定的方法一定都要在你的Class里面找得到,不然会报错
- methods数组中的方法签名需要注意要对应Java层的方法,具体规则可以查看https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/types.html
- 最后返回想使用的JNI的版本
这样我们就完成了方法的绑定了
这两种方式推荐使用动态绑定的方式,因为这种方式更加灵活,而且效率更高,唯一的缺点就是需要写的代码比静态的方式要多点吧
Callback的注册方式
接下来我们来示例一种callback的方式
在Java层的MainNativeInterface类增加一个方法,并增加callback的interface
public interface JNICallback {
void result(String message);
}
public native void callAsyncMethod(JNICallback callback);
JNI层onLoad方法中动态绑定方法
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
javaVM = vm;
jclass c = env->FindClass("com/example/nativeevaldemo/MainNativeInterface");
if (c == nullptr) return JNI_ERR;
static const JNINativeMethod methods[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void*) stringFromJNI},
{"callAsyncMethod", "(Lcom/example/nativeevaldemo/MainNativeInterface$JNICallback;)V", (void*) callAsyncMethod}
};
int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod));
if (rc != JNI_OK) return rc;
return JNI_VERSION_1_6;
}
static void
callAsyncMethod(JNIEnv *env, jclass thiz, jobject callback) {
jclass clazz = env->GetObjectClass(callback);
jmethodID jmid = env->GetMethodID(clazz, "result", "(Ljava/lang/String;)V");
env->CallVoidMethod(callback, jmid, env->NewStringUTF("this is callback result message"));
}
callAsyncMethod方法中,可以通过传入的callback的object取得class类(即传入进来的接口的实现类),然后找到class类里面的result方法的Id,然后通过callxxxMethod方法调用
使用上
MainNativeInterface.callAsyncMethod {
Log.d("NativeTestActivity", "result message == $it")
}
调用callAsyncMethod 方法输出 这边的callback只是一个简单的回调方法的说明,真正使用中也许需要增加线程来做到异步的处理或者是在其他的.c或.cpp文件中进行回调的处理,这个时候就需要将此callback维护起来
扩展
有没有好奇的小伙伴们注意到,如果我们即写了显示主动注册又写了隐式被动注册会如何? 这边大家不妨也可以试试,其实是会使用主动注册方式绑定的,因为我们知道隐式被动注册会在调用方法的时候才去查找绑定native方法,而且,只要绑定过了一次后,就不会再次绑定,而显示主动注册方式的onLoad会先执行,先为native方法绑定
RegisterNatives方法也可以不在onLoad方法中调用,我们可以这样做来实验一下,我们先去除onLoad的注册方法
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
用隐式被动注册来注册一个方法,并且在其内部使用RegisterNatives方法注册绑定另一个我们显示的native方法
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_nativeevaldemo_MainNativeInterface_stringFromJNI(JNIEnv *env, jclass thiz) {
jclass c = env->FindClass("com/example/nativeevaldemo/MainNativeInterface");
static const JNINativeMethod methodss[] = {
{"stringFromJNI", "()Ljava/lang/String;", (void*) stringFromJNI}
};
int rc = env->RegisterNatives(c, methodss, sizeof(methodss)/sizeof(JNINativeMethod));
return env->NewStringUTF("native auto search method bind");
}
static jstring
stringFromJNI(JNIEnv *jenv, jclass ths){
return jenv->NewStringUTF("native hand method bind");
}
我们调用该方法2次,会发现该方法的绑定会被切换 是不是很有趣呢
|