《深入理解Android卷 I》- 第二章 - JNI - 读书笔记

此文章为《深入理解Android卷 I》的读书笔记,笔者已经完成了卷一的第一遍阅读,第一遍时写下了一些笔记,现在开始第二遍阅读,借此记录自己的阅读笔记,其中涉及的代码已经参考7.0修改

1 加载动态库(.so)

在加载JNI库是只需要加载名字,实际加载会拓展成.so(unix-like) 或者.dll(windows)

System.loadLibrary("media_jni");

1.1 声明native函数

使用Java关键字native即表明该函数由JNI完成

private static native final void native_init();

1.2 何时加载

如果java要调用native函数,就必须通过一个位于JNI层的动态库才能做到,所以就必须要加载

原则上是在调用native函数钱,任何时候任何地方都可以加载,通常做法是在类的static语句中加载,通过System.loadLibrary方法就可以了。

1.3 总结

使用JNI需要完成两个任务就可以了

  • 加载对应的JNI库
  • 声明关键字native修饰的函数

2 JNI层的MediaScanner分析

MediaScanner的JNI代码在(frameworks\base\media\jni\android_media_MediaScanner.cpp)
Java层声明的native_init()函数对应的JNI函数为android_media_MediaScanner_native_init(JNIEnv *env)
实现如下:

static void android_media_MediaScanner_native_init(JNIEnv *env)
{
   jclass clazz = env->FindClass(kClassMediaScanner);
   if (clazz == NULL) return;
   fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
   if (fields.context == NULL) return;
}

2.1 注册JNI函数

  • 静态方法
    根据函数名来找对应的JNI函数。这种方法需要Java的工具程序Javah参与:
  • 编写Java代码编译成.class文件;
  • 使用Javah,例(javah -o output packagename.calssname),这样他会生成output.h的JNI层头文件。其中packagename.classname是Java代码编译后的.class文件,而在生成的output.h文件里,声明了对应JNI函数,只要实现里面的函数即可。一般头文件名字会使用packagename_class.h的样式,事例中提到的JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile

    7.0源码中已经改为下面的注册方式
  • 动态注册
    JNI只一个叫JNINativeMethod的结构来记录java native和JNI函数的一一对应关系
    libnativehelper/include/nativehelper/jni.h
typedef struct {
    //Java中native函数的名字,不用携带包的路径。例如“native_init“。
    const char* name;
    //Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。
    const char* signature;
    void* fnPtr; //JNI层对应函数的函数指针,注意它是void*类型。
} JNINativeMethod;

2.1.1 动态注册的使用

以MediaScanner为例 (frameworks/base/media/jni/android_media_MediaScanner.cpp)
建立对应关系:

//定义一个JNINativeMethod数组,其成员就是MediaService中所有native函数的一一对应关系。
static JNINativeMethod gMethods[] = {
    ......
    {
        "processFile" //Java中native函数的函数名。
        //processFile的签名信息,签名信息的知识,后面再做介绍。
        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
        (void*)android_media_MediaScanner_processFile //JNI层对应函数指针。
    },
    ......
    {
        "native_init",
        "()V",
        (void *)android_media_MediaScanner_native_init
    },
    ......
};

注册:

// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp
int register_android_media_MediaScanner(JNIEnv *env)
{
    //调用AndroidRuntime的registerNativeMethods函数,第二个参数表明是Java中的哪个类
    return AndroidRuntime::registerNativeMethods(env, kClassMediaScanner, gMethods, NELEM(gMethods));
}

AndroidRunTime类提供了一个registerNativeMethods函数来完成注册工作,实现如下:frameworks/base/core/jni/AndroidRuntime.cpp

int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods){
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

方法继续调用了 jniRegisterNativeMethods()
libnativehelper/JNIHelp.cpp

extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,const JNINativeMethod* gMethods, int numMethods){
    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
    ALOGV("Registering %s's %d native methods...", className, numMethods);
    scoped_local_ref<jclass> c(env, findClass(env, className));
    if (c.get() == NULL) {
        char* tmp;
        const char* msg;
        if (asprintf(&tmp,"Native registration unable to find class '%s'; aborting...", className) == -1) {
            // Allocation failed, print default warning.
            msg = "Native registration unable to find class; aborting...";
        } else {
            msg = tmp;
        }
        e->FatalError(msg);
    }
    //实际上是调用JNIEnv的RegisterNatives函数完成注册的
    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
        char* tmp;
        const char* msg;
        if (asprintf(&tmp, "RegisterNatives failed for '%s'; aborting...", className) == -1) {
            // Allocation failed, print default warning.
            msg = "RegisterNatives failed; aborting...";
        } else {
            msg = tmp;
        }
        e->FatalError(msg);
    }
    return 0;
}

2.1.2 JNI注册总结

完成动态注册只要两个函数就能完成:

  • jclass clazz = (*env)->FindClass(env, className);
    env指向一个JNIEnv结构体,classname为对应的Java类名,由于
    JNINativeMethod中使用的函数名并非全路径名,所以要指明是哪个类。
  • 调用JNIEnv的RegisterNatives函数,注册关联关系。
    (*env)->RegisterNatives(env, clazz, gMethods,numMethods);

当自己使用的时候,在什么时候什么地方调用?

  • 当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数,如果有,就调用它,而动态注册的工作就是在这里完成的。

    所以,如果想使用动态注册方法,就必须要实现JNI_OnLoad函数,只有在这个函数中,才有机会完成动态注册的工作。一些初始化操作也在这里完成。

2.1.3 注册实例

libmedia_jni.soJNI_OnLoad函数在android_media_MediaPlayer.cpp中实现
frameworks/base/media/jni/android_media_MediaPlayer.cpp

jint JNI_OnLoad(JavaVM* vm, void* /* reserved */){
//该函数的第一个参数类型为JavaVM,这可是虚拟机在JNI层的代表喔,每个Java进程只有一个这样的JavaVM
    JNIEnv* env = NULL;
    jintresult = -1;
    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) goto bail;
    ... //很多注册其他jni函数的操作
    //动态注册MediaScanner的JNI函数。
    if(register_android_media_MediaScanner(env) < 0) goto bail;
    .... //很多注册其他jni函数的操作
    returnJNI_VERSION_1_4;//必须返回这个值,否则会报错。
}    

2.2 数据类型转换

Java数据类型分为基本数据类型和引用数据类型两种,JNI层也是区别对待这二者的。

2.2.1 基本数据类型

Java native类型 符号属性 字长
boolean jboolean 无符号 8位
byte jbyte 无符号 8位
char jchar 无符号 16位
short jshort 无符号 16位
int jint 有符号 32位
long jlong 有符号 64位
float jfloat 有符号 32位
double jdouble 有符号 64位

需要注意对应的字长的变化,jchar在Native语言中是16位,占两个字节,这和普通的char占一个字节的情况完全不一样。

2.2.2 引用数据类型

Java引用类型 native类型
ALL objects jobject
java.lang.Class jclass
java.lang.String jstring
Object[] jobjectArray
boolean[] jbooleanArray
byte[] jbyteArray
java.lang.Throwable ithrowable
char[] jcharArray
short[] jshortArray
int[] jintArray
long[] jlongArray
float[] jfloatArray
double[] jdoubleArray

由上表可知:

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

示例

//Java层processFile有三个参数。
processFile(String path, StringmimeType,MediaScannerClient client);
//JNI层对应的函数,最后三个参数和processFile的参数对应。
android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz,
jstring path, jstring mimeType, jobject client)

由上可发现:

  • Java的String类型在JNI层对应为jstring。
  • Java的MediaScannerClient类型在JNI层对应为jobject。
  • Java中的processFile只有三个参数,JNI层对应的函数五个参数?
    第二个参数jobject代表Java层的MediaScanner对象,如果Java层是static函数的话,那么这个参数将是jclass,表示是在调用哪个Java Class的静态函数。

3 JNIEnv介绍

JNIEnv是一个和线程相关的,代表JNI环境的结构体,Context?上下文?

15-08-35.jpg

JNIEnv实际就是提供了一些JNI系统函数。通过这些函数可以做到:

  • 调用Java的函数
  • 操作jobject对象等很多事

线程相关:
线程A有一个JNIEnv,线程B有一个JNIEnv。由于线程相关,所以不能在线程B中使用线程A的JNIEnv结构体。

当后台线程收到一个网络消息,而又需要由Native层函数主动回调Java层函数时,我们需要JNIEnv,但是我们不能保存一个JNIEnv,所以需要从javaVM获取,在JNI_Onload中的第一个参数就是,它是虚拟机在JNI层的代表,进程所有

  • 调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数了。
  • 后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源。再来看JNIEnv的作用。

4 JNIEnv操作jobject

4.1 jfieldID和jmethodID

在JNI中所有的Java对象都是jobject,需要知道属性和方法必须通过JNIEnv来获得。
在JNI中,用jfieldIDjmethodID来表示Java的类成员和函数
jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);

  • jclass clazz 当前类
  • const char*name 变量或者函数名
  • const char*sig 变量或者函数签名

4.2 使用jfeildID和jmethodID

4.2.1 使用jmethodID

普通方法mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,lastModified, fileSize)
通过JNIEnv输出的CallVoidMethod,再把jobjectjMethodID和对应参数传进去,JNI层就能够调用Java对象的函数了!实际上JNIEnv输出了一系列类似CallVoidMethod的函数,形式如下:NativeType Call<type>Method(JNIEnv *env,jobject obj,jmethodID methodID, ...)type是对应Java函数的返回值类型。
静态方法:调用Java中的static函数,则用JNIEnv输出的CallStatic<Type>Method系列函数。

4.2.2 使用jfeildID

获得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)

5 jstring介绍

Java中的String也是引用类型,不过由于它的使用非常频繁,所以在JNI规范中单独创建了一个jstring类型来表示Java中的String类型。需要JNIEnv提供的函数来操作:

  • 调用JNIEnv的NewString(JNIEnv *env, const jchar*unicodeChars,jsize len),可以从Native的字符串得到一个jstring对象。其实,可以把一个jstring对象看成是Java中String对象在JNI层的代表,也就是说,jstring就是一个Java String。
  • 调用JNIEnv的NewStringUTF(const char* bytes)将根据Native的一个UTF-8字符串得到一个jstring对象。在实际工作中,这个函数用得最多。
  • GetStringChars(jstring string, jboolean* isCopy)GetStringUTFChars(jstring string, jboolean* isCopy)函数,它们可以将JavaString对象转换成本地字符串。
  • 在代码中调用了上面几个函数,在做完相关工作后,就都需要调用ReleaseStringChars(jstring string, const jchar* chars)ReleaseStringUTFChars(jstring string, const char* utf)函数对应地释放资源,否则会导致JVM内存泄露。这一点和jstring的内部实现有关系。

6 JNI类型签名介绍

static JNINativeMethod gMethods[] = {
    ......
    {
        "processFile"
         //processFile的签名信息
        "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
        (void*)android_media_MediaScanner_processFile
    },
    ......
}

Java中对应函数的签名信息,由参数类型和返回值类型共同组成。为什么需要这个签名信息呢?

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

Java中函数定义:void processFile(String path, StringmimeType,MediaScannerClient client);
对应的JNI函数签名就是:(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V
括号内是参数类型的标示,最右边是返回值类型的标示,void类型对应的标示是V。

类型标示 Java类型
Z boolean
B byte
C char
S short
I int
J long
F float
D double
[I int []
L/java/lang/String; Stirng
L/java/lang/Object; object

其他类似,对象后面需要加;号,如果数组还需要在前面加[
示例

函数签名 Java函数
()Ljava/lang/String; String f()
(ILjava/lang/Class;)J long f(int i, Class c)
([B)V void f(byte[] bytes)

7 垃圾回收

三种类型引用:

  • LocalReference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference。它包括函数调用时传入的jobject、在JNI层函数中创建的jobject。LocalReference最大的特点就是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。
  • Global Reference:全局引用,这种对象如不主动释放,就永远不会被垃圾回收。
  • Weak Global Reference:弱全局引用,一种特殊的GlobalReference,在运行过程中可能会被垃圾回收。所以在程序中使用它之前,需要调用JNIEnv的IsSameObject判断它是不是被回收了。
    平时用得最多的是Local ReferencGlobal Reference.
    每当JNI层想要保存Java层中的某个对象时,就可以使用Global Reference,使用完后记住释放它就可以了。
    JNIenv->New[xxxx]Ref()创建引用,Delete[xxxx]Ref()释放内存

示例
frameworks/base/media/jni/android_media_MediaScanner.cpp :: MyMediaScannerClient

class MyMediaScannerClient : public MediaScannerClient{
    public:
    MyMediaScannerClient(JNIEnv *env, jobject client)
        :mEnv(env),
        //调用NewGlobalRef创建一个GlobalReference,这样mClient就不用担心回收了。
        mClient(env->NewGlobalRef(client)),
        mScanFileMethodID(0),
        mHandleStringTagMethodID(0),
        mSetMimeTypeMethodID(0)
    {
    ...
    }
    virtual ~MyMediaScannerClient()
    {
        //调用DeleteGlobalRef释放这个全局引用。
        mEnv->DeleteGlobalRef(mClient);
    }
}

8 JNI中的异常处理

调用JNIEnv的某些函数出错后,会产生一个异常,但这个异常不会中断本地函数的执行,直到从JNI层返回到Java层后,虚拟机才会抛出这个异常。虽然在JNI层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能做一些资源清理工作了JNI层函数可以在代码中截获和修改这些异常,JNIEnv提供了三个函数进行帮助:

  • ExceptionOccured()函数,用来判断是否发生异常。
  • ExceptionClear()函数,用来清理当前JNI层中发生的异常。
  • ThrowNew(jclass clazz, const char* message)函数,用来向Java层抛出异常。
    下一篇 《第三章 - init》读书笔记

推荐阅读更多精彩内容

  • 什么是JNI? JNI 是java本地开发接口.JNI 是一个协议,这个协议用来沟通java代码和外部的本地代码(...
    a_tomcat阅读 1,988评论 0 55
  • _ 声明: 对原文格式以及内容做了细微的修改和美化, 主要为了方便阅读和理解 _ 一. 基础 Java Nativ...
    元亨利贞o阅读 4,949评论 0 33
  • JNI概述 Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++写的函数 Nat...
    cfryan1990阅读 5,376评论 4 8
  • 不知道是不是有点迟,当朋友圈铺天盖地都在刷《我的前半生》时,我才后知后觉想去一探究竟。不得不说其中充满了离婚、小三...
    阿妩遇见阅读 235评论 0 0
  • 第一次打心眼里折服于文字是看了一篇孔云峰写的文章,一篇关于卡瓦格博的文章。 壮丽魅惑的梅里雪山,炙热的个人英雄主义...
    岁月一声笑阅读 560评论 0 2
  • 接连几日被江歌案缠绕,今天又看到杀老师的新闻,心灵遭受万点针刺般的细小伤,无形却痛,忧伤如雪花般落满了头。 怎么办...
    晔红阅读 72评论 0 1
  • 早上起来,空腹不吃东西,是我一天精神最好的时候,这个时候做什么事情都是顺利的。 有时候有些冷,还刻意不想给自己穿太...
    周公子聊娱乐阅读 121评论 1 4