程诺陪你学AndroidAndroid

JNI知识总结

2022-05-28  本文已影响0人  三十五岁养老

概念

JNI:Java本地调用 ,是Java Native Interface的缩写。JNI是一种技术,可以做到以下两点:

为什么需要jni

基本使用

public native String stringFromJNI();
static {
        System.loadLibrary("JniDemo");
    }

JniDemo是JNI库的名字。实际加载动态库的时候会拓展成libJniDemo.so,在Windows平台上将拓展为JniDemo.dll。

public class MainActivity extends AppCompatActivity {

    // Used to load the 'JniDemo' library on application startup.
    static {
        System.loadLibrary("JniDemo");
    }

    private ActivityMainBinding binding;

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

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

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(stringFromJNI()); // java代码调用native函数
    }

    /**
     * A native method that is implemented by the 'JniDemo' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_JniDemo_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}
  1. extern "C"根据需要动态添加,如果是C++代码,则必须要添加extern “C”声明,如果是C代码,则不用添加。(extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。)
  2. JNIEXPORT 这个关键字说明这个函数是一个可导出函数,C/C++ 库里面的函数有些可以直接被外部调用,有些不可以,原因就是每一个C/C++库都有一个导出函数列表,只有在这个列表里面的函数才可以被外部直接调用,类似Java的public函数和private函数的区别。 JNI层必须实现为动态库的形式,这样Java虚拟机才能加载它并调用它的函数。
  3. 说明这个函数是一个JNI函数,用来和普通的C/C++函数进行区别,实际发现不加这个关键字,Java也是可以调用这个JNI函数的。
  4. jstring 这个函数的返回值是jstring
  5. Java_com_example_JniDemo_MainActivity_stringFromJNI(JNIEnvenv, jobject thiz)这是完整的JNI函数声明,JNI函数名的原型如下:
    Java_ + JNI方法所在的完整的类名,把类名里面的”.”替换成”_” + 真实的JNI方法名,这个方法名要和Java代码里面声明的JNI方法名一样+ JNI函数必须的默认参数(JNIEnv
    env, jobject thiz), env参数是一个指向JNIEnv函数表的指针,thiz参数代表的就是声明这个JNI方法的Java类的引用

JNI调用流程

通过原生Android代码分析流程
java: MediaCrypto.java
jni:android_media_MediaCrypto.cpp

MediaCrypto.java 部分代码

 99        private static native final void native_init();  //声明一个native函数。native为Java的关键字,表示它将由JNI层完成。
100         
101  
102     
104      static {
105          System.loadLibrary("media_jni");  //加载对应的JNI库,media_jni是JNI库的名字
106          native_init(); //调用 jni native_init函数
107      }
108  
android_media_MediaCrypto.cpp  部分代码

 static void android_media_MediaCrypto_native_init(JNIEnv *env) {  //这个函数是native_init的JNI层实现。
160      jclass clazz = env->FindClass("android/media/MediaCrypto");
161      CHECK(clazz != NULL);
162  
163      gFields.context = env->GetFieldID(clazz, "mNativeContext", "J");
164      CHECK(gFields.context != NULL);
165  }

android_media_MediaCrypto_native_init是native_init的jni 层实现, java层和native层需要将这2个函数绑定形成关联关系,系统才能找到它们,也就是接下来要说的注册.

注册

动态注册

在JNI技术中,用JNINativeMethod的结构来记录对应关系,其定义如下

typedef struct {
   const char* name;      //Java中native函数的名字,不用携带包的路径。例如“native_init“。
   const char* signature;//Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。
   void*      fnPtr;  //JNI层对应函数的函数指针,注意它是void*类型。
} JNINativeMethod;

如何使用这个数据结构呢,看下对应 android_media_MediaCrypto.cpp 文件代码

6  static const JNINativeMethod gMethods[] = {
307      { "release", "()V", (void *)android_media_MediaCrypto_release },
308      { "native_init",  Java中native函数的函数名。
                "()V",  native_init签名信息,后面再做介绍
                (void *)android_media_MediaCrypto_native_init //JNI层对应函数指针。
             }, 
310  
312      ...
324  };
325  
326  int register_android_media_Crypto(JNIEnv *env) { //注册JNINativeMethod数组
327      return AndroidRuntime::registerNativeMethods(env,
328                  "android/media/MediaCrypto", gMethods, NELEM(gMethods));
329  }
int jniRegisterNativeMethods(JNIEnv* env, const char* className,
320      const JNINativeMethod* methods, int numMethods)
321  {
322      ALOGV("Registering %s's %d native methods...", className, numMethods);
323      jclass clazz = (*env)->FindClass(env, className);
324      ALOG_ALWAYS_FATAL_IF(clazz == NULL,
325                           "Native registration unable to find class '%s'; aborting...",
326                           className);
327      int result = (*env)->RegisterNatives(env, clazz, methods, numMethods);
328      (*env)->DeleteLocalRef(env, clazz);
329   ...
347  }

注册函数register_android_media_Crypto调用时机

1463  jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
1464  {
1465      JNIEnv* env = NULL;
1466      jint result = -1;
1467  
1468      if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
1469          ALOGE("ERROR: GetEnv failed\n");
1470          goto bail;
1471      }
1472      assert(env != NULL);
1473  
1474      if (register_android_media_ImageWriter(env) != JNI_OK) {
1475          ALOGE("ERROR: ImageWriter native registration failed");
1476          goto bail;
1477      }
              ...
1554      if (register_android_media_Crypto(env) < 0) {
1555          ALOGE("ERROR: MediaCodec native registration failed");
1556          goto bail;
1557      }
           }

当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数,如果有,就调用它,而动态注册的工作就是在这里完成的。所以,如果想使用动态注册方法,就必须要实现JNI_OnLoad函数,只有在这个函数中,才有机会完成动态注册的工作

静态注册

静态注册是根据函数名来找对应的JNI函数,它要求JNI层函数的名字必须遵循特定的格式, 函数名规则如下
Java_ + JNI方法所在的完整的类名,把类名里面的”.”替换成”_” + 真实的JNI方法名,这个方法名要和Java代码里面声明的JNI方法名一样+ JNI函数必须的默认参数(JNIEnv* env, jobjectthiz)
//native_init对应的JNI函数

//Java层函数名中如果有一个”_”的话,转换成JNI后就变成了”_l”。
JNIEXPORT void JNICALL Java_android_media_MediaCrypto_native_1init(JNIEnv* env, jclass thiz); 

当Java层调用native_init函数时,它会从对应的JNI库查找Java_android_media_MediaCrypto_native_linit,如果没有,就会报错。如果找到,则会为这个native_init和Java_android_media_MediaCrypto_native_linit建立一个关联关系,其实就是保存JNI层函数的函数指针。以后再调用native_init函数时,直接使用这个函数指针就可以了,当然这项工作是由虚拟机完成的。
对应的jni头文件可以手写,不过比较麻烦, 可以使用 javah工具自动生成

弊端:

  1. 编译所有声明了native函数的Java类,每个生成的class文件都得用javah生成一个头文件。
  2. javah生成的JNI层函数名特别长,书写起来很不方便。
  3. 初次调用native函数时要根据函数名字搜索对应的JNI层函数来建立关联关系,这样会影响运行效率。

jdk10已经移除javah工具,相应的功能已经集成到javac中,你可以试试javac -h替代javah。

数据类型

在Java中调用native函数传递的参数是Java数据类型,这些参数类型到了JNI层会变成JNI对应的数据类型
Java数据类型分为基本数据类型和引用数据类型两种,JNI层也是区别对待这二者的。

基本数据类型转换关系表
捕获.PNG
引用类型对照表
捕获.PNG

除了Java中基本数据类型的数组、Class、String和Throwable外,其余所有Java对象的数据类型在JNI中都用jobject表示。

静态JNI方法和实例JNI方法参数区别
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_JniDemo_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_JniDemo_MainActivity_stringFromJNI2(JNIEnv *env, jclass clazz) {
    // TODO: implement stringFromJNI2()
}

普通的JNI方法对应的JNI函数的第二个参数是jobject类型,而静态的JNI方法对应的JNI函数的第二个参数是jclass类型

JNIEnv的认识

概念

在JNI世界里离不开JNIEnv,JNIEnv是一个和线程相关的,代表JNI环境的结构体


image.png

JNIEnv提供了一些JNI系统函数,通过这些函数可以

JNIEnv,是一个和线程有关的变量。线程A有一个JNIEnv,线程B有一个JNIEnv。由于线程相关,所以不能在线程B中使用线程A的JNIEnv结构体。当后台线程收到一个网络消息,需要由Native层函数主动回调Java层函数时,JNIEnv是从何而来呢?根据前面的介绍可知,我们不能保存另外一个线程的JNIEnv结构体,然后把它放到后台线程中来用。

前面提到过JNI_OnLoad函数,第一个参数是JavaVM,它是虚拟机在JNI层的代表。不论检查中多少个线程,JavaVM独此一份,在任意地方都可以使用它。

如果我们需要在其他线程访问JVM,那么必须先调用AttachCurrentThread将当前线程与JVM进行关联,然后才能获得JNIEnv对象。

JavaVMAttachArgs args = {JNI_VERSION_1_4, NULL, NULL};
JavaVM* vm = AndroidRuntime::getJavaVM();
int result = vm->AttachCurrentThread(&env, (void*) &args);

当然,我们在必要时需要调用DetachCurrentThread来解除链接。

 JavaVM* vm = AndroidRuntime::getJavaVM();
int result = vm->DetachCurrentThread();

使用

通过JNIEnv操作jobject

Java的引用类型除了少数几个外,最终在JNI层都用jobject来表示对象的数据类型,操作jobject的本质是操作这些对象的成员变量和成员函数
JNI规则中,用jfieldID 和jmethodID 来表示Java类的成员变量和成员函数

通过JNIEnv的下面两个函数可以得到:

jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);

jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);
 static void android_media_MediaCrypto_native_init(JNIEnv *env) {
160      jclass clazz = env->FindClass("android/media/MediaCrypto");
161      CHECK(clazz != NULL);
162  
163      gFields.context = env->GetFieldID(clazz, "mNativeContext", "J");
164      CHECK(gFields.context != NULL);
165  }

接下来就是使用

 static sp<JCrypto> setCrypto(
142          JNIEnv *env, jobject thiz, const sp<JCrypto> &crypto) {
143      sp<JCrypto> old = (JCrypto *)env->GetLongField(thiz, gFields.context);
144      if (crypto != NULL) {
145          crypto->incStrong(thiz);
146      }
147      if (old != NULL) {
148          old->decStrong(thiz);
149      }
150      env->SetLongField(thiz, gFields.context, (jlong)crypto.get());
151  
152      return old;
153  }

实际上JNIEnv输出了一系列类似GetLongField的函数,形式如下:
//获得fieldID后,可调用Get<type>Field系列函数获取jobject对应成员变量的值。
NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)

//或者调用Set<type>Field系列函数来设置jobject对应成员变量的值。
void Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)

//下面我们列出一些参加的Get/Set函数。
GetObjectField() SetObjectField()
GetBooleanField() SetBooleanField()

如果想调用Java中的static 字段,则用JNIEnv输出的GetStatic<Type>Field系列函数。

同理 通过JNIEnv操作jobject的成员函数和字段类似
形式如下:
NativeType Call<type>Method(JNIEnv *env,jobject obj,jmethodID methodID, ...)

调用Java中的static函数
JNIEnv输出的CallStatic<Type>Method系列函数

jstring

Java中的String也是引用类型,不过由于它的使用非常频繁,所以在JNI规范中单独创建了一个jstring类型来表示Java中的String类型。Java中的String包含很多成员函数,但是jstring是一种独立的数据类型,并没有提供成员函数供操作。

操作jstring得依靠JNIEnv提供的帮助。
可以把一个jstring对象看成是Java中String对象在JNI层的代表,也就是说,jstring就是一个Java String

JNI类型签名

动态注册中的数组信息

static const JNINativeMethod gMethods[] = {
307      { "release", "()V", (void *)android_media_MediaCrypto_release },
308      { "native_init", "()V", (void *)android_media_MediaCrypto_native_init },
309  
310      { "native_setup", "([B[B)V",
311        (void *)android_media_MediaCrypto_native_setup },
312  
313      { "native_finalize", "()V",
314        (void *)android_media_MediaCrypto_native_finalize },
315  
316      { "isCryptoSchemeSupportedNative", "([B)Z",
317        (void *)android_media_MediaCrypto_isCryptoSchemeSupportedNative },
318  
319      { "requiresSecureDecoderComponent", "(Ljava/lang/String;)Z",
320        (void *)android_media_MediaCrypto_requiresSecureDecoderComponent },
321  
322      { "setMediaDrmSession", "([B)V",
323        (void *)android_media_MediaCrypto_setMediaDrmSession },
324  };

(void *)android_media_MediaCrypto_native_init 为函数native_init的签名信息,由参数类型和返回值类型共同组成
格式为:
(参数1类型标示参数2类型标示...参数n类型标示)返回值类型标示

因为Java支持函数重载,也就是说,可以定义同名但不同参数的函数。但仅仅根据函数名,是没法找到具体函数的。为了解决这个问题,JNI技术中就使用了参数类型和返回值类型的组合,作为一个函数的签名信息,有了签名信息和函数名,就能很顺利地找到Java中的函数了。

类型标示示意表
捕获.PNG

签名信息可以通过以下几种方式获取:

垃圾回收

从上面2条结论可得知,当jni层通过 赋值 “=” 保存Java层传入的jobject对象,在某个对象调用时,java层可能已经释放对象

但是JNI规范已很好地解决了这一问题,JNI技术一共提供了三种类型的引用,它们分别是:

JNI常用函数

参考 https://blog.csdn.net/qinjuning/article/details/7595104

异常处理

当JNI函数调用的Java方法出现异常的时候,并不会影响JNI方法的执行,但是我们并不推荐JNI函数忽略Java方法出现的异常继续执行,这样可能会带来更多的问题。我们推荐的方法是,当JNI函数调用的Java方法出现异常的时候,JNI函数应该合理的停止执行代码。

参考文章

JNI完全指南
深入理解JNI
JNI基础语法

上一篇 下一篇

猜你喜欢

热点阅读