JNI使用手册,其二,基础篇。

JNI基本概念

JNI定义

  • JNI (Java Native Interface)
    • 是一种在Java虚拟机控制下执行代码的标准机制;
    • 是native code的编程接口;
    • 是允许Java代码和其他语言代码进行双向交互;

jni_role.png

JNI优势和劣势

  • 优势
    • 能够重用native代码;
    • 实现可用类库中缺少的功能;
    • 提高执行性能或其他与环境相关的系统特性;
    • 满足跨包调用、绕过Java安全性检查等特殊情况;
  • 劣势
    • Java到Native Code的上下文切换耗时、低效;
    • 会触发JVM崩溃和内存泄漏;

JNI元素

JNI提供两种主要数据结构,”JavaVM“和”JNIEnv“,其本质上都是指向函数表指针的指针。

  • JavaVM

    • JavaVM是JVM在JNI层的代表,JNI全局只有一个;
    • JavaVM提供执行接口函数,使用这些函数可以创建和销毁JavaVM;
    • 理论上每个进程可以有多个JavaVM,但Android只运行一个;
  • JNIEnv

    • JNIEnv是线程相关的结构体,代表Java在本线程的运行环境,每个线程都有一个,JNI中可能有多个JNIEnv;
    • 通过JNIEnv可以调用Java代码,操作传入的Java对象;
    • JNIEnv用于线程本地存储,不能在线程之间共享JNIEnv变量。若无法获取到某个线程的JNIEnv,则需要共享JavaVM,调用GetEnv来获取JNIEnv;
    • Native方法的参数,比Java函数声明多出两个,如(JNIEnv *, jobject, jstring),JNIEnv作为第一个参数
      • 若原生函数非静态函数,则第二个参数是对对象的引用;
      • 若原生函数是静态函数,则第二个参数是对Java类的引用;

JNIEnv图示

jni_struct

  • JNIEnv接口指针,指向一个线程相关的结构,类似C++的虚函数表,该结构包含了指向函数表的指针,函数表的每个入口包含一个指向JNI函数的指针;
  • JNI接口指针仅在当前线程中起作用,指针不能从一个线程进入另一个线程,但可以在不同的线程中调用本地方法;不要跨线程传递JNI接口指针;

JNI线程模型

  • 线程基础

    • Native线程由操作系统调度;Java线程由JavaVM调度;
    • Native线程受具体操作系统的限制,Java线程可以跨平台,若两边的线程和同步概念存在不一致,程序将无法正常执行;
  • 线程执行

    • Native代码执行在其调入的Java方法所在的调用栈中;
    • 调用栈从Java层到Native层,JNI不会改变调用栈,也不会改变线程环境,除非开发者指定;
    • Android不会挂起正在执行本地代码的线程,如果当前垃圾回收器正在运行,或者调试器遇到问题需要挂起,Android在下次JNI调用时才会暂停线程;
  • 线程转换

    • Java层调用Native方法
      • 将会创建一个栈帧(Stack Frame)存储JNIEnv指针等VM信息
      • 线程结束时无需调用DetachCurrentThread;
    • Native层通过pthread_create等方式创建的线程调用Java方法
      • 需要调用AttachCurrentThread或AttachCurrentThreadAsDaemon,依附到JavaVM上,从而获取到JNIEnv指针;
      • Native中线程的创建,由pthread_create转换为Android::createJavaThread;
      • 在已经attach的线程上调用AttachCurrentThread是个多余操作;
      • 线程退出时需调用DetachCurrentThread;

JNI调用和传递

Native方法命名

  • 前缀Java_
  • 完整类名(类名中的._代替)
  • 下划线_
  • 方法名(方法名中的特殊字符需要转义)
  • 参数签名(非必须,有重载方法的时候才需要),如果有重载的本地方法,需要再添加两个下划线__,然后再添加方法签名(由java字段描述符描述,用_代替描述符中的包名分割/符,签名中的特殊字符需要转义)

转义字符

转义符 说明
_0XXXX 一个Unicode字符XXXX。注意小写是用来表示非ascii Unicode字符, 如:_0abcd与_0ABCD不相同
_1 字符_
_2 参数签名中的字符;
_3 参数签名中的字符[

域描述符

  • 基本类型
Java类型 本地类型(JNI) 域描述符 描述
boolean jboolean Z 无符号8bit
byte jbyte B 有符号8bit
char jchar C 无符号16bit
short jshort S 有符号16bit
int jint I 有符号32bit
long jlong J 有符号64bit
float jfloat F 32bit
double jdouble D 64bit
void void V N/A
  • 数组,[,如:int[]-> [I
  • 对象
    • L开头,以;结尾,中间是用/隔开的包及类名。如:Ljava/lang/String;
    • 如果是嵌套类,则用$来表示嵌套。如:Landroid/os/FileUtils$FileStatus;

JNI对象管理

  • 对象传递

    • 基础数据类型直接映射,如int映射成jint,在Java和Native之间是采用值传递;
    • 引用类型以一种不透明的引用方式向Native传递对象;
  • 内存回收

    • Java以类似引用计数的方式管理对象;
    • JVM必须要追踪所有传到Native Code的Java对象;
    • Native Code在使用Java对象时,需要确保该对象在使用过程中不被回收;
    • Native Code需要能够通知JVM不再需要某些Java对象;
    • JVM在适当的时机触发GC(Garbage Collection)操作,清理不再使用的对象;
  • 对象映射

    • 当线程从Java环境切换到Native Code上下文,JVM分配一块内存,为每一个从Java到Native Method的过渡控制建立一个注册表,表中存放本次native method执行中创建的所有局部引用;
    • 运行native method的线程堆栈,记录局部引用表的内存位置;
    • 局部引用表实现局部引用到Java对象的映射;当Native Code中引用到一个Java对象,JVM在表中创建一个局部引用,引用计数+1,阻止了所引用的对象被回收;
    • DeleteLocalRef释放局部引用,从表中删除引用,即减少相应Java对象的引用计数;
    • 在native method执行完毕后,所有的局部引用被删除,生命周期结束;
    • 局部引用表存在容量限制,初始大小64,最大为512;超出会引起报错:local reference table overflow (max=512);
  • 局部引用

    • 传入native方法的所有参数,通过NewLocalRef和各种JNI接口创建的对象都是局部引用;
    • 不能跨函数、跨线程使用;
    • 尽早调用DeleteLocalRef,避免潜在内存泄漏;
    • 可以用PushLocalFrame和PopLocalFrame更方便地管理局部引用;
    • 理解局部引用和native code中局部变量的区别;
      • 存储位置,一个在线程堆栈,一个在局部引用表;
      • 生命周期;
      • 访问限制,局部引用需要借助JNI函数实现间接访问;
        // Java
        public Object func() {...}
        // JNI
        jobject obj = env->CallObjectMethod(...);
    
  • 另一种局部引用管理方式

    jobject func(JNIEnv *env, ...)
    {
        jobject result;
        if (env->PushLocalFrame(10) < 0)
        {
            /* 调用PushLocalFrame获取10个局部引用失败 */
            return NULL;
        }
        ...
        result = ...; // 创建局部引用result
        if (...)
        {
            /* 返回前先弹出栈顶的frame */
            result = env->PopLocalFrame(result);
            return result;
        }
        ...
        result = env->PopLocalFrame(result);
        /* 正常返回 */
        return result;
    }
    
  • 全局引用

    • 全局引用表容量存在限制,初始大小为512,最大为51200;
    • 调用NewGlobalRef基于局部引用创建,长期保存一个对象避免被自动GC;
    • 可以跨函数、跨线程使用;
    • 会阻止JVM自动GC回收该对象;必须调用DeleteGlobalRef手动释放;
    • NewGlobalRef操作了同一个对象,但是引用并不是同一个;
    • jfield和jmethod是透明类型,GetStringUtfChars和GetByteArrayElements返回的原始数据指针,都不是对象;
        jobject localRef = xxx;
        jobject globalRef = env->NewGlobalRef(localRef);
        env->DeleteLocalRef(localRef);
    
  • 弱全局引用

    • 调用NewWeakGlobalRef基于局部引用或全局引用创建,不会阻止JVM自动GC回收该对象;
    • 引用不会自动释放,需要调用DeleteWeakGlobalRef手动释放;或者在JVM认为应该回收时被回收释放,但其在引用表中所占用的内存不会被回收;
    • 可以跨函数、跨线程;
  • 引用比较

    • env->IsSameObject(obj1,obj2),可以判定两个引用是否指向同一对象,相同返回JNI_TRUE(1),否则返回JNI_FALSE(0);
    • JNI中的NUL引用,指向Java中的null对象;
    • IsSameObject用于弱全局引用与NULL的比较时,返回值意义不同,JNI_TRUE表示指向的引用已被回收,JNI_FALSE表示指向一个活动对象;

参考