JNI知识总结

程序员 2024-9-19 16:30:12 170 0 来自 中国
概念

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

  • Java步调中的函数可以调用Native语言写的函数,Native一样平常指的是C/C++编写的函数。
  • ·Native步调中的函数可以调用Java层的函数,也就是在C/C++步调中可以调用Java的函数。
为什么须要jni


  • C/C++语言已经有了许多成熟的模块,Java只须要直接调用即可。尚有一些寻求服从和速率的场所,须要Native参与。
  • Java语言是平台无关,但是承载Java世界的假造机是用Native语言写的,而假造机又运行在详细平台上,以是假造机本身无法做到平台无关,JNI技能可以针对Java层屏蔽差别操作系统之间的差别,如许就可以或许实现平台无关特性。

    1.png
基本使用


  • 在Java代码内里声明Native方法原型,好比
public native String stringFromJNI();

  • java静态代码加载so库( JNI层必须实现为动态库的情势,如许Java假造机才华加载它并调用它的函数)
static {        System.loadLibrary("JniDemo");    }JniDemo是JNI库的名字。实际加载动态库的时候会拓展成libJniDemo.so,在Windows平台上将拓展为JniDemo.dll。

  • java代码调用native函数
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();}

  • 在C/C++代码内里声明JNI方法原型并实现
extern "C" JNIEXPORT jstring JNICALLJava_com_example_JniDemo_MainActivity_stringFromJNI(        JNIEnv* env,        jobject /* this */) {    std::string hello = "Hello from C++";    return env->NewStringUTF(hello.c_str());}

  • extern "C"根据须要动态添加,如果是C++代码,则必须要添加extern “C”声明,如果是C代码,则不消添加。(extern "C"的重要作用就是为了可以或许精确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部门代码按C语言的举行编译,而不是C++的。)
  • JNIEXPORT 这个关键字分析这个函数是一个可导出函数,C/C++ 库内里的函数有些可以直接被外部调用,有些不可以,缘故起因就是每一个C/C++库都有一个导出函数列表,只有在这个列表内里的函数才可以被外部直接调用,类似Java的public函数和private函数的区别。 JNI层必须实现为动态库的情势,如许Java假造机才华加载它并调用它的函数。
  • 分析这个函数是一个JNI函数,用来宁静常的C/C++函数举行区别,实际发现不加这个关键字,Java也是可以调用这个JNI函数的。
  • jstring 这个函数的返回值是jstring
  • 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  

  • 加载JNI库
    Java要调用Native函数,就必须通过一个位于JNI层的动态库才华做到。加载动态库的机遇原则上是在调用native函数前,任何时候、任何地方加载都可以。通行的做法是,在类的static语句中加载,通过调用System.loadLibrary方法就可以了。
  • Java的native函数
    从上面代码中可以发现,native_init函数前有Java的关键字native,它表现这两个函数将由JNI层来实现。、
  • 只要完成下面两项工作就可以使用JNI了:
    ·  1. 加载对应的JNI库。
    ·  2. 声明由关键字native修饰的函数。
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  }

  • 界说一个JNINativeMethod数组,其成员就是全部native函数的逐一对应关系。
  • 注册JNINativeMethod数组 registerNativeMethods .第二个参数表明是Java中的哪个类
    AndroidRunTime类提供了一个registerNativeMethods函数来完成注册工作,实际上是调用JNIEnv的RegisterNatives函数完成注册的
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工具自动生成

  • 先编写Java代码,然后编译生成.class文件。
  • 使用Java的工具步调javah,如javah–o output packagename.classname ,如许它会生成一个叫output.h的JNI层头文件。此中packagename.classname是Java代码编译后的class文件,而在生成的output.h文件里,声明白对应的JNI层函数,只要实现内里的函数即可。头文件的名字一样平常都会使用packagename_class.h的样式,比方MediaCrypto对应的JNI层头文件就是android_media_MediaCrypto.h
弊端:

  • 编译全部声明白native函数的Java类,每个生成的class文件都得用javah生成一个头文件。
  • javah生成的JNI层函数名特别长,书写起来很不方便。
  • 初次调用native函数时要根据函数名字搜刮对应的JNI层函数来创建关联关系,如许会影响运行服从。
jdk10已经移除javah工具,相应的功能已经集成到javac中,你可以试试javac -h替换javah。
数据类型

在Java中调用native函数通报的参数是Java数据类型,这些参数类型到了JNI层会酿成JNI对应的数据类型
Java数据类型分为基本数据类型和引用数据类型两种,JNI层也是区别对待这二者的。
基本数据类型转换关系表

2.png 引用类型对照表


除了Java中基本数据类型的数组、Class、String和Throwable外,其余全部Java对象的数据类型在JNI中都用jobject表现。
静态JNI方法和实例JNI方法参数区别

extern "C" JNIEXPORT jstring JNICALLJava_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 JNICALLJava_com_example_JniDemo_MainActivity_stringFromJNI2(JNIEnv *env, jclass clazz) {    // TODO: implement stringFromJNI2()}平常的JNI方法对应的JNI函数的第二个参数是jobject类型,而静态的JNI方法对应的JNI函数的第二个参数是jclass类型
JNIEnv的认识

概念

在JNI世界里离不开JNIEnv,JNIEnv是一个和线程相干的,代表JNI情况的布局体

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

  • 调用Java函数【jni层调用java层】
  • 操作jobject对象等许多事变【jni层调用native】
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);

  • jclass代表Java类(成员函数和成员变量都是类的信息)
  • name表现成员函数或成员变量的名字
  • 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  }

  • 先找到android/media/MediaCrypto类在JNI层中对应的jclass实例
  • 取出MediaCrypto类中变量mNativeContext的jfieldID
  • 将变量mNativeContext的jfieldID 生存起来,由于每次操作jobject前都去查询jmethoID或jfieldID的话将会影响步调运行的服从
接下来就是使用
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函数,第一个是代表MediaCrypto的jobject对象,第二个参数是mNativeContext的jfieldID
  • 通过JNIEnv输出的GetLongField,再把jobject和jMethodID(如果对应参数)传进去,JNI层就可以或许调用Java对象的字段了!
实际上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

  • C/C++字符串转JNI字符串
    NewString函数用来生成Unicode JNI字符串
    NewStringUTF函数用来生成UTF-8 JNI字符串
  • JNI字符串转C/C++字符串
    GetStringChars函数用来从jstring获取Unicode C/C++字符串
    GetStringUTFChars函数用来从jstring获取UTF-8 C/C++字符串
  • 如果在代码中调用了上面几个函数,在做完相干工作后,就都须要调用
    ReleaseStringChars或ReleaseStringUTFChars函数对应地开释资源,否则会导致JVM内存走漏。
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类型标示)返回值类型标示

  • 括号内是参数类型的标示,最右边是返回值类型的标示,
  • 当参数的类型是引用类型时,其格式是”L包名;”(标示末了有一个“;”),此中包名中的”.”换成”/”
  • 如果Java类型是数组,则标示中会有一个“[”
由于Java支持函数重载,也就是说,可以界说同名但差别参数的函数。但仅仅根据函数名,是没法找到详细函数的。为了办理这个题目,JNI技能中就使用了参数类型和返回值类型的组合,作为一个函数的署名信息,有了署名信息和函数名,就能很顺遂地找到Java中的函数了。
类型标示表现表

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

  • 对照java函数手写署名,不外轻易写错
  • 通过AndroidStudio 新建C++工程 自动生成
  • 通过javap工具,javap –s -p xxx,此中xxx为编译后的class文件,s表现输出内部数据类型的署名信息,p表现打印全部函数和成员的署名信息,而默认只会打印public成员和函数的署名信息。
垃圾接纳


  • Java中创建的对象末了是由垃圾接纳器来接纳和开释内存的(引用计数和可达性)
  • JNI层使用save_thiz = thiz 如许的语句,是不会增长引用计数的
从上面2条结论可得知,当jni层通过 赋值 “=” 生存Java层传入的jobject对象,在某个对象调用时,java层大概已经开释对象
但是JNI规范已很好地办理了这一题目,JNI技能一共提供了三种类型的引用,它们分别是:

  • Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference。它包罗函数调用时传入的jobject、在JNI层函数中创建的jobject。LocalReference最大的特点就是,一旦JNI层函数返回,这些jobject就大概被垃圾接纳。
    如果不调用DeleteLocalRef,pathStr将在函数返回后被接纳;如果调用DeleteLocalRef的话,pathStr会立刻被接纳。
  • Global Reference:(env->NewGlobalRef(client)) )全局引用,这种对象如不自动开释,就永世不会被垃圾接纳,调用DeleteGlobalRef开释这个全局引用。
  • Weak Global Reference:弱全局引用,一种特别的GlobalReference,在运行过程中大概会被垃圾接纳。以是在步调中使用它之前,须要调用JNIEnv的IsSameObject判断它是不是被接纳了。
JNI常用函数

参考 https://blog.csdn.net/qinjuning/article/details/7595104
非常处理处罚

当JNI函数调用的Java方法出现非常的时候,并不会影响JNI方法的实验,但是我们并不保举JNI函数忽略Java方法出现的非常继承实验,如许大概会带来更多的题目。我们保举的方法是,当JNI函数调用的Java方法出现非常的时候,JNI函数应该公道的制止实验代码。

  • ExceptionOccurred函数用来判断JNI函数调用的Java方法是否出现非常
  • ExceptionClear函数用来扫除JNI函数调用的Java方法出现的非常
    Java代码:

    6.png
    JNI代码:


    JNI通过ThrowNew函数抛出Java类型的非常

参考文章

JNI完全指南
深入明白JNI
JNI根本语法
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2025-2-6 14:07, Processed in 0.184888 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表