Android JNI

136 阅读8分钟

NDK

Android NDK(Native Develop Kit)是一套允许您使用原生代码语言(例如C和C++)实现部分应用的工具集

为什么使用NDK

  1. 重复使用现有库,现在许多第三方库都是由C/C++库编写的,比如Ffmpeg这样库
  2. 在某些情况下提性能,特别是像游戏这种计算密集型应用
  3. 代码的保护。由于APK的Java层代码很容易被反编译,而C/C++库反编译难度大
  4. 不依赖于Java虚拟机的设计(堆大小限制)

不同的CPU架构

目前Android系统支持以下七种不同的CPU架构,每一种对应着各自的应用程序二进制接口ABI:(Application Binary Interface)定义了二进制文件如何运行在相应的系统平台上,从使用的指令集,内存对齐到可用的系统函数库。对应关系如下:

  • ARMv5——armeabi
  • ARMv7 ——armeabi-v7a
  • ARMv8——arm64-v8a
  • x86——x86
  • x86_64——x86_64
  • MIPS ——mips
  • MIPS64——mips64

JNI原理

在计算机系统中,每一种编程语言都有一个执行环境(Runtime),执行环境用来解释执行语言中的语句,不同的编程语言的执行环境就好比神话世界中的“阴阳两界"一样,一般人不能同时生存在阴阳两界中,只有一些特殊的仙人——"黑白无常"才能自由穿梭在"阴阳两界",而"黑白无常"往返于阴阳两界时手持生日薄,"黑白无常"按生死薄上记录的任命来"索魂"

JavaVM

Java语言的执行环境是Java虚拟机(JVM),JVM其实是主机环境中的一个进程,每个JVM虚拟机都在本地环境中有一个JavaVM结构体,该结构体在创建Java虚拟机时被返回,在JNI环境中创建JVM的函数为JNI_CreateJavaVM

JNI_CreateJavaVM(JavaVM **pvm, void **penv, void*args);

其中JavaVM是Java虚拟机在JNI层的代表,JNI全局仅仅有一个JavaVM结构中封装了一些函数指针(或叫函数表结构),JavaVM中封装的这些函数指针主要是对JVM操作接口

JNIEnv

JNIEnv是当前Java线程的执行环境,一个JVM对应一个JavaVM结构,而一个JVM中可能创建多个Java线程,每个线程对应一个JNIEnv结构,它们保存在线程本地存储TLS中。因此,不同的线程的JNIEnv不同,也不能相互共享使用。JNIEnv结构也是一个函数表,在本地代码中通过JNIEnv的函数表来操作Java数据或者调用Java方法。也就是说,只要在本地代码中拿到了JNIEnv结构,就可以在本地代码中调用Java代码

JNIEnv的作用

  • 调用Java函数:JNIEnv代表了Java执行环境,能够使用JNIEnv调用Java中的代码
  • 操作Java代码:Java对象传入JNI层就是jobject对象,需要使用JNIEnv来操作这个Java对象

JNIEnv与线程

JNIEnv是线程相关的,即在每一个线程中都有一个JNIEnv指针,每个JNIEnv都是线程专有的,其他线程不能使用本线程中的JNIEnv,即线程A不能调用线程B的JNIEnv。所以JNIEnv不能跨线程

  • JNIEnv只在当前线程有效:JNIEnv仅仅在当前线程有效,JNIEnv不能在线程之间进行传递,在同一个线程中,多次调用JNI层方法,传入的JNIEnv是同样的
  • 本地方法匹配多个JNIEnv:在Java层定义的本地方法,能够在不同的线程调用,因此能够接受不同的JNIEnv

与JNIEnv相关的常用函数

  • jobject NewObject(JNIEnv *env, jclass clazz,jmethodID methodID, ...):
  • jobject NewObjectA(JNIEnv *env, jclass clazz,jmethodID methodID, const jvalue *args):
  • jsize GetArrayLength(JNIEnv *env, jarray array);
#include <jni.h>

// 假设这是一个 Java 类的本地方法
extern "C" JNIEXPORT void JNICALL Java_com_example_MyClass_nativeMethod(JNIEnv *env, jobject obj) {
    // 获取 Java 类 MyClass
    jclass clazz = env->GetObjectClass(obj);

    // 获取 MyClass 的构造函数的 methodID
    jmethodID constructor = env->GetMethodID(clazz, "<init>", "()V");

    // 使用 NewObject 创建一个 MyClass 的新对象
    jobject newObj = env->NewObject(clazz, constructor);

    // 使用 NewObjectA 创建一个 MyClass 的新对象(假设构造函数需要传入参数)
    // 假设 MyClass 的构造函数是 MyClass(int, String)
    jvalue args[2];
    args[0].i = 42; // int 参数
    args[1].l = env->NewStringUTF("Hello"); // String 参数
    jobject newObjA = env->NewObjectA(clazz, constructor, args);

    // 使用新对象进行操作...

    // 释放局部引用
    env->DeleteLocalRef(newObj);
    env->DeleteLocalRef(newObjA);
}

jvalue

  • jvalue 是一个结构体,用于在 JNI 中表示 Java 方法的参数和返回值,定义如下:
        typedef union jvalue {
            jboolean z;
            jbyte    b;
            jchar    c;
            jshort   s;
            jint     i;
            jlong    j;
            jfloat   f;
            jdouble  d;
            jobject  l;
        } jvalue;
  • jvalue 中的不同成员对应不同的 Java 数据类型。例如 jint 对应 Java 的 int 类型,jobject 对应 Java 对象类型
  • 在 JNI 中,当你需要传递参数给 Java 方法或从 Java 方法中获取返回值时,通常会使用 jvalue 结构体

JNI的引用

Java内存管理这块是完全透明的,new一个实例时,只知道创建这个类的实例后,会返回这个实例的一个引用,然后拿着这个引用去访问它的成员(属性、方法),完全不用管JVM内部是怎么实现的,如何为新建的对象申请内存,使用完之后如何释放内存,只需要知道有个垃圾回收器在处理这些事情就行了,然而,从Java虚拟机创建的对象传到C/C++代码就会产生引用,根据Java的垃圾回收机制,只要有引用存在就不会触发该该引用所指向Java对象的垃圾回收

在JNI规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)

1、局部引用(Local Reference)

局部引用,也成本地引用,通常是在函数中创建并使用。会阻止GC回收所有应用对象。

最常见的引用类型,基本上通过JNI返回来的引用都是局部引用,例如使用NewObject,就会返回创建出来的实例的局部引用,局部引用值在该native函数有效,所有在该函数中产生的局部引用,都会在函数返回的时候自动释放(freed),也可以使用DeleteLocalRef函数手动释放该应用。之所以使用DeleteLocalRef函数:实际上局部引用存在,就会防止其指向对象被垃圾回收期回收,尤其是当一个局部变量引用指向一个很庞大的对象,或是在一个循环中生成一个局部引用,最好的做法就是在使用完该对象后,或在该循环尾部把这个引用释放掉,以确保在垃圾回收器被触发的时候被回收。在局部引用的有效期中,可以传递到别的本地函数中,要强调的是它的有效期仍然只是在第一次的Java本地函数调用中,所以千万不能用C++全部变量保存它或是把它定义为C++静态局部变量。

2、全局引用(Global Reference)

全局引用可以跨方法、跨线程使用,直到被开发者显式释放。类似局部引用,一个全局引用在被释放前保证引用对象不被GC回收。和局部应用不同的是,没有那么多函数能够创建全局引用。能创建全部引用的函数只有NewGlobalRef,而释放它需要使用ReleaseGlobalRef函数

3、弱全局引用(Weak Global Reference)

是JDK 1.2 新增加的功能,与全局引用类似,创建跟删除都需要由编程人员来进行,这种引用与全局引用一样可以在多个本地代码有效,不一样的是,弱引用将不会阻止垃圾回收期回收这个引用所指向的对象,所以在使用时需要多加小心,它所引用的对象可能是不存在的或者已经被回收

通过使用NewWeakGlobalRef、ReleaseWeakGlobalRef来产生和解除引用

注册native函数

当Java代码中执行Native的代码的时候,首先是通过一定的方法来找到这些native方法。而注册native函数的具体方法不同,会导致系统在运行时采用不同的方式来寻找这些native方法

JNI有如下两种注册native方法的途径:

  • 静态注册: 先由Java得到本地方法的声明,然后再通过JNI实现该声明方法
  • 动态注册: 先通过JNI重载JNI_OnLoad()实现本地方法,然后直接在Java中调用本地方法

参考文献