Android JNI 介绍

837 阅读7分钟

什么是JNI

JNI是Java Native Interface的缩写

顾名思义,JNI是Java世界和Native世界的桥梁,这里的Native主要是指C和C++。

为什么要学习JNI

这个问题很好回答,因为Android系统源码中大量的使用了JNI技术.如果你只停留在Java层,那么永远不会真正理解Android, 虽然你到了Native层也不一定会理解。

怎么学习JNI

前面说了,Android Aosp中大量的使用了JNI, 那我们可以找个模块学习. 如果你是做Android Tv相关的,可以看TvInput模块和Tune Framework模块,这里选择后者。

loadLibrary

JNI的逻辑一般是动态库中实现的,Linux下一般是.so文件,Windows下是.dll文件,下面以Android Tv的Tuner Framework框架介绍一下加载动态库的过程,先找到Tuner这个java类,里面有个静态代码块,System.loadLibrary就是我们加载动态库的操作,Android中加载的是so库,参数就是我们要加载的so库的名称。

//frameworks/base/media/java/android/media/tv/tuner/Tuner.java  
static {
        try {
            System.loadLibrary("media_tv_tuner");
            nativeInit();
        } catch (UnsatisfiedLinkError e) {
            Log.d(TAG, "tuner JNI library not found!");
        }
    }

上面media_tv_tuner加载的是下面的so库,so文件前面加了个lib.

/system/lib/libmedia_tv_tuner.so

这里,关于System.loadLibrary的过程,后面会单独出一篇文章介绍.

Java中定义Jni函数

在Java类中定义jni函数,只需要添加native关键字,而他的实现则是在上面加载的so库中(jni的实现可以是c也可以是c++,Aosp中主要用的是c++)。

  //frameworks/base/media/java/android/media/tv/tuner/Tuner.java 
  /**
     * Native setup.
     */
    private native void nativeSetup();

Jni层中的实现

//frameworks/base/media/jni/android_media_tv_Tuner.cpp
static void android_media_tv_Tuner_native_setup(JNIEnv *env, jobject thiz) {
    sp<JTuner> tuner = new JTuner(env, thiz);
    setTuner(env,thiz, tuner);
}

上面就是java层nativeSetup()方法的实现,咦~~~,有没有发现那里不对劲,定义的方法和实现的方法怎么不一样,而且还多了两个参数。一个在Java层一个在Native层怎么是怎么互相找到呢?别急,这里要讨论两种注册方式: 静态注册和动态注册。首先为啥要注册,注册就是让java层中定义的native方法和so中对应的方法绑定起来,生成一个函数映射,这样就能彼此找到了。

静态注册的方式

通过特殊的规则定义的Jni函数名称:以Java为前缀,并且用“_”下划线将包名、类名以及native方法名连接起来

Java_packagename_classname_methodname(JNIEnv *env,jclass/jobject,...)

静态注册的步骤:

  • 在Java中定义native方法
  • 用javah 和javac命令生成包含native方法的.h头文件
  • 实现native方法

使用静态注册的方式,在运行的时候需要根据函数名称去找相关联的函数,效率比较低。而且函数的命名必须的符合上述的规则.

动态注册

静态注册方法太长了,可以使用动态注册的方式。通常我们在JNI_OnLoad方法中完成动态注册。

JNI_OnLoad

调用System.loadLibrary()函数时, 内部就会去查找so中的 JNI_OnLoad 函数,如果存在此函数,则调用。 JNI_OnLoad 必须返回 JNI 的版本,比如 JNI_VERSION_1_6、JNI_VERSION_1_8。

jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("ERROR: GetEnv failed\n");
        return result;
    }
    assert(env != NULL);

    if (!register_android_media_tv_Tuner(env)) {
        ALOGE("ERROR: Tuner native registration failed\n");
        return result;
    }
    return JNI_VERSION_1_4;
}
static bool register_android_media_tv_Tuner(JNIEnv *env) {
    if (AndroidRuntime::registerNativeMethods(
            env, "android/media/tv/tuner/Tuner", gTunerMethods, NELEM(gTunerMethods)) != JNI_OK) {
        ALOGE("Failed to register tuner native methods");
        return false;
    }
  ···
}
static const JNINativeMethod gTunerMethods[] = {
    { "nativeInit", "()V", (void *)android_media_tv_Tuner_native_init },
    { "nativeSetup", "()V", (void *)android_media_tv_Tuner_native_setup },
    { "nativeGetFrontendIds", "()Ljava/util/List;"
      ...
    }

通过AndroidRuntime::registerNativeMethods将Java层中的native方法和Jni层的c++方法映射起来,传入的参数就是一个JNINativeMethod数组, 上面的JNI_OnLoad方法我们需要稍微详细的讲一下,这个方法给我们带来了两个概念JavaVM和JNIEnv.

  • JavaVM

这个代表java的虚拟机。所有的工作都是从获取虚拟机的接口开始的。在Android一个进程对应一个JavaVM.

  • JNIEnv

是提供JNI Native函数的基础环境,线程相关,不同线程的JNIEnv相互独立。JNIEnv只在当前线程中生效,本地方法不能将JNIEnv从一个线程传递到另外一个线程,所以不要缓存JNIEnv*.通过JNIEnv可以方便的调用Java函数和操作jobject对象,本文后面会介绍一些JNIEnv操作Java函数的一些用法。

数据类型

前面的注册解决了方法的映射,那么还有个问题就是数据类型的映射。

  • 基本数据类型的映射
Java数据类型Jni数据类型C++类型
booleanjbooleanbool
intjintint
longjlonglong
bytejbytebype
charjcharchar
shortjshortshort
floatjfloatfloat
doublejdoubledouble
  • 引用类型

    Java引用类型Jni类型
    Objectjobject
    Classjclass
    Stringjstring
    Object[]jobjectArray
    bype[]jbypeArray
    Throwablejthrowable

如果想看完整的Jni类型,可以在

libnativehelper/include_jni/jni.h

中查看.

/* Primitive types that match up with Java equivalents. */
typedef uint8_t  jboolean; /* unsigned 8 bits */
typedef int8_t   jbyte;    /* signed 8 bits */
typedef uint16_t jchar;    /* unsigned 16 bits */
typedef int16_t  jshort;   /* signed 16 bits */
typedef int32_t  jint;     /* signed 32 bits */
typedef int64_t  jlong;    /* signed 64 bits */
typedef float    jfloat;   /* 32-bit IEEE 754 */
typedef double   jdouble;  /* 64-bit IEEE 754 */

/* "cardinal indices and sizes" */
typedef jint     jsize;

#ifdef __cplusplus
/*
 * Reference types, in C++
 */
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};

typedef _jobject*       jobject;
typedef _jclass*        jclass;
typedef _jstring*       jstring;
typedef _jarray*        jarray;
typedef _jobjectArray*  jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray*    jbyteArray;
typedef _jcharArray*    jcharArray;
typedef _jshortArray*   jshortArray;
typedef _jintArray*     jintArray;
typedef _jlongArray*    jlongArray;
typedef _jfloatArray*   jfloatArray;
typedef _jdoubleArray*  jdoubleArray;
typedef _jthrowable*    jthrowable;
typedef _jobject*       jweak;

Java层调用Jni层

在Java层已经定义了native方法,并且和native方法做了映射,这个使用比较简单, 直接调用java的方法就可以实现Java层调用Jni层了。

Jni层怎么返回一个Java对象

一个Java对象本质是由成员变量和成员函数构成的,操作一个Java对象就是操作其成员函数和成员变量.比如下面的代码,

我们在Jni层创建一个String对象。

  /*
* convert c++ string to jstring
*/
    static jstring stringTojstring(JNIEnv *env, const char *pat)
    {
        jclass strClass = env->FindClass("java/lang/String");
        jmethodID ctorID = env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");
        jbyteArray bytes = env->NewByteArray(strlen(pat));
        (env)->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte *)pat);
        jstring encoding = env->NewStringUTF("UTF-8");
        return (jstring)env->NewObject(strClass, ctorID, bytes, encoding);
    }
  • FindClass

    获取jclass对象,用于获取函数的ID和创建实例,这个过程和我们Java使用反射的流程是一样的。

  • GetMethodID

    获取MethodID, 用于后面的调用,这里的""代表构造函数,后面构造函数的签名,[B代表第一个参数是byte数组,Ljava/lang/String; 代表第二个参数是一个String类型,V代表返回值是Void,后面会给一张表介绍函数签名。

  • NewByteArray

    生成一个byte数组对象

  • SetByteArrayRegion

    给byte数组赋值。

  • NewObject

    生成构造方法的形式生成我们的String对象

整个流程其实和反射非常的相似。

Jni层调用一个Java方法

下面的例子演示一下怎么在Jni中调用一下ArrayList的add方法,添加一个Integer对象。

  jclass arrayListClazz = env->FindClass("java/util/ArrayList");
  jmethodID arrayListAdd = env->GetMethodID(arrayListClazz, "add", "(Ljava/lang/Object;)Z");
 jobject obj = env->NewObject(arrayListClazz, env->GetMethodID(arrayListClazz, "<init>", "()V"));
    jclass integerClazz = env->FindClass("java/lang/Integer");
    jmethodID intInit = env->GetMethodID(integerClazz, "<init>", "(I)V");

    for (int i=0; i < 10; i++) {
       jobject idObj = env->NewObject(integerClazz, intInit, i);
       env->CallBooleanMethod(obj, arrayListAdd, idObj);
    }
  • GetMethodID

    获取add方法的method id.

  • CallBooleanMethod

    obj代表ArrayList对象,arrayListAdd是add的method id, idObj是add的参数,这里是一个Integer对象。

Native线程回调Java层

前面说了JNIEnv是线程相关的,如果我们想在一个native的线程里回调到Java层,因为回调Java层需要用到JNIEnv,而这个时候我们又不能用别的线程里的JNIEnv,这该怎么办呢?

通过下面的方式:

JNIEnv *env = nullptr;
JavaVM->AttachCurrentThread(&env, NULL);

我们知道JavaVM是进程中唯一的,通过JavaVMAttachCurrentThread方法,就可以得到这个线程的JNIEnv。通过JNIEnv就可以回到Java层了。

完事以后记得

JavaVM->DetachCurrentThread();

类型签名

前面在动态注册方法的时候,其实我们已经使用了方法签名,

static const JNINativeMethod gTunerMethods[] = {
    { "nativeInit", "()V", (void *)android_media_tv_Tuner_native_init },
    { "nativeSetup", "()V", (void *)android_media_tv_Tuner_native_setup },
    { "nativeGetFrontendIds", "()Ljava/util/List;"
      ...
    }

比如"()V"代表参数为空,返回值为V.,参数在括号里,返回值在括号外,参数需要按顺序排放,如果参数是引用类型则以L开头, 跟全类名再加分号(;),例如:Ljava/util/List;

下面是类型签名表:

签名类型
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble
L全类名;类名
[签名类型 例如:[I 代表int[]数组
Vvoid

签名很容易写错,java提供了一个javap的工具,可以生成签名信息,用法如下:

javap -s youclass.class

总结

Jni在系统开发中,一般给app封装接口时用到,但是,更重要的,作为Java世界和Native世界的纽带,它是我们学习Android系统源码的必备技能。

关于我

  • 公众号: CodingDev

qrcode_for_gh_0e16b0c63d2d_258.jpg