代码改变世界

《深入理解Android(卷1)》笔记 1.第二章 深入理解JNI

2012-09-08 00:31  ...平..淡...  阅读(948)  评论(0编辑  收藏  举报

第一章就跳过了,比较基础。

第二章   深入理解JNI

知识点1:JNI概述

JNI:Java Native Interface,中译为“java本地接口”。Native语言一般指C/C++。

JNI技术可以实现java与C/C++之间的互通(java程序<----调用---->C/C++程序)。

虽然java语言是平台无关的,但JNI的推出基于以下几个方面的考虑:

(1)java虚拟机是Native语言写的,所以虚拟机本身是平台相关的。而有了JNI技术,就可以对java层屏蔽不同操作系统间的差异了。java程序只管通过调用Native方法实现相应功能就行,无需理会平台差异,就实现了java本身的平台无关性。(ps:书中说“其实java一直在使用JNI技术,只是我们平时较少用到罢了”)

(2)在一些要求效率和速度的场合还是需要Native语言参与其中的。

这也就说明了JNI技术的作用: Java世界 <------> JNI层 <-------> Native世界


知识点2:通过分析MediaScanner源码来学习JNI

首先来看一下涉及到的部分:

Java (MediaScanner)
JNI (libmedia_jni.so)
Native (libmedia.so)

 

 

 

分析:

* Java层     对应的是MediaScanner类。   这个类有一些函数需要Native层来实现。
* JNI层      对应的是libmedia_jni.so。  medi_jni是JNI库的名字(Android一般用“lib模块名_jni.so”的命名方式来命名JNI库的名字)。
* Native层   对应的是libmedia.so。      这个库完成了实际的功能。
MediaScanner将通过JNI库libmedia_jni.so和Native层的libmedia.so交互。

 

Java层的MediaScanner分析

public class MediaScanner
{
    static {
        //加载JNI库
        System.loadLibrary("media_jni");
        native_init();
    }
    ......
    //声明native函数。native是java的关键字,表示它将由JNI层完成
    private native void processDirectory(String path, MediaScannerClient client);
    private native void processFile(String path, String mimeType, MediaScannerClient client);
    private static native final void native_init();

所以要想使用JNI,只需做两件事:

1.加载JNI库

2.声明由关键字native修饰的函数

 

JNI层的MediaScanner分析

JNI注册的两种方法

(1)静态方法:根据函数名来找对应的JNI函数,找到后,就建立一个关联关系(即保存JNI层函数的函数指针)。以后调用就直接使用这个函数指针(这项工作由虚拟机完成)。

ps:如何确定java中的native函数对应的JNI函数:java中native的全路径名是包名.类名.方法名(如MediaScanner类中native_init方法的全路径名是android.media.MediaScanner.native_init)。又因为"."符号在Native语言中有特殊意义,故JNI中,将"."换成"_"。故native_init方法对应的JNI层方法为android_media_MediaScanner_native_init.

 静态注册的流程:

1.编写Java代码(比如创建好了HelloJNI.java)
2.编译Java代码
javac HelloJNI.java  //生成了HelloJNI.class文件

3.生成C语言头文件

使用java的工具程序javah,生成一个JNI层头文件,里面声明了该java代码中的native函数对应的JNI层函数,因此就可以调用JNI层中具体的函数了。
javah -jni HelloJNI  //生成HelloJNI.h文件。

4.编写C代码

将HelloJNI.h文件中声明的函数,在hellojni.c中实现。
 
5.生成C共享库
通过以下指令生成.so动态库文件:
gcc -I "/usr/lib/jvm/java-6-sun-1.6.0.26/include" -I "/usr/lib/jvm/java-6-sun-1.6.0.26/include/linux" -shared hellojni.c -o libhellojni.so
其实指令原理就是:
gcc -I "<JDK_HOME>/include" -I"<JDK_HOME>/include/linux" -shared hellojni.c -o libhellojni.so

6.运行Java程序

java -Djava.library.path=. HelloJNI  
//—Djava.library.path这个参数必须设置(设置生成的动态库的位置),否则会报找不到动态库。

代码如下:

HelloJNI.java/HelloJNI.h/hellojni.c
//编写HelloJNI.java

class HelloJNI
{
    native void printHello();
    native void printString(String str);
    static {System.loadLibrary("hellojni");}
    public static void main(String args[])
    {
        System.out.println(System.getProperty("java.library.path"));

        HelloJNI myJNI = new HelloJNI();
        myJNI.printHello();
        myJNI.printString("Hello world!");
//        System.out.println(System.getProperty("java.library.path"));
    }

}



//通过javah生成的HelloJNI.h

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */

#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    printHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_HelloJNI_printHello
  (JNIEnv *, jobject);

/*
 * Class:     HelloJNI
 * Method:    printString
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_HelloJNI_printString
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif




//将HelloJNI.h中的函数,在hellojni.c中实现

#include "HelloJNI.h"
#include <stdio.h>

JNIEXPORT void JNICALL Java_HelloJNI_printHello
  (JNIEnv *env, jobject obj)
{
    printf("hello,world in printHello\n");
    return;
}

/*
 * Class:     HelloJNI
 * Method:    printString
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_HelloJNI_printString
  (JNIEnv *env, jobject obj, jstring string)
{
    const char *str = (*env)->GetStringUTFChars(env,string,0);
    printf("%s\n",str);
    return;
}

 

--------------------------------------------------------------------------------------

(2)动态方法:直接让native函数知道JNI层对应函数的函数指针。

 动态注册

 通过JNINativeMethod 结构体来存储native函数和JNI函数之间的关联关系。

typedef struct {
    //Java中native函数的名字,不用携带包的路径,例如“native_init”
    const char* name;
    //Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合
    const char* signature;
    //JNI层对应函数的函数指针,注意它是void* 类型
    void* fnPtr;
} JNINativeMethod;

 JNINativeMethod在android.media.MediaScanner.cpp中的使用

//1、创建关系对应数组
static
JNINativeMethod gMethods[] = { { "processDirectory", "(Ljava/lang/String;Landroid/media/MediaScannerClient;)V", (void *)android_media_MediaScanner_processDirectory }, { "processFile", "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V", (void *)android_media_MediaScanner_processFile }, ...... { "native_init", "()V", (void *)android_media_MediaScanner_native_init }, ...... }; ......
......
// 2、通过register_android_media_MediaScanner方法来注册JNINativeMethod数组
// 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) { return AndroidRuntime::registerNativeMethods(env, "android/media/MediaScanner", gMethods, NELEM(gMethods)); }

该方法调用AndroidRunTime.cpp提供的registerNativeMethods方法,如下所示

/*
 * Register native methods using JNI.
 */
int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

该方法又会调用jniRegisterNativeMethods方法,该方法在dalvik/libnativehelper/JNIHelp.c中,它是Android为方便JNI使用而提供的一个帮助函数。如下所示

/*
 * Register native JNI-callable methods.
 *
 * "className" looks like "java/lang/String".
 */
int jniRegisterNativeMethods(JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    jclass clazz;

    LOGV("Registering %s natives\n", className);
    clazz = (*env)->FindClass(env, className); // 1
    if (clazz == NULL) {
        LOGE("Native registration unable to find class '%s'\n", className);
        return -1;
    }
    if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0) {  // 2
        LOGE("RegisterNatives failed for '%s'\n", className);
        return -1;
    }
    return 0;
}

 动态注册的工作,主要就是通过两个函数完成:

(1)FindClass(env, className):找到对应的java类。

(2)RegisterNatives(env, clazz, gMethods, numMethods):注册关联关系。

 那动态注册的函数什么时候和什么地方被调用呢?

the result isJava层通过System.loadLibrary加载完JNI动态库后,接着会查找该库中的JNI_OnLoad函数,如果有,就调用它,动态注册的工作就是在JNI_OnLoad函数中完成的。

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

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

    if (register_android_media_MediaPlayer(env) < 0) {
        LOGE("ERROR: MediaPlayer native registration failed\n");
        goto bail;
    }

    if (register_android_media_MediaRecorder(env) < 0) {
        LOGE("ERROR: MediaRecorder native registration failed\n");
        goto bail;
    }
    //动态注册MediaScanner的JNI函数
    if (register_android_media_MediaScanner(env) < 0) {
        LOGE("ERROR: MediaScanner native registration failed\n");
        goto bail;
    }

    if (register_android_media_MediaMetadataRetriever(env) < 0) {
        LOGE("ERROR: MediaMetadataRetriever native registration failed\n");
        goto bail;
    }

    if (register_android_media_AmrInputStream(env) < 0) {
        LOGE("ERROR: AmrInputStream native registration failed\n");
        goto bail;
    }

    if (register_android_media_ResampleInputStream(env) < 0) {
        LOGE("ERROR: ResampleInputStream native registration failed\n");
        goto bail;
    }

    if (register_android_media_MediaProfiles(env) < 0) {
        LOGE("ERROR: MediaProfiles native registration failed");
        goto bail;
    }

    if (register_android_mtp_MtpDatabase(env) < 0) {
        LOGE("ERROR: MtpDatabase native registration failed");
        goto bail;
    }

    if (register_android_mtp_MtpDevice(env) < 0) {
        LOGE("ERROR: MtpDevice native registration failed");
        goto bail;
    }

    if (register_android_mtp_MtpServer(env) < 0) {
        LOGE("ERROR: MtpServer native registration failed");
        goto bail;
    }

    /* success -- return valid version number */
    result = JNI_VERSION_1_4;

bail:
    return result;
}

 


 

知识点3: 数据类型转换

在java中调用的native函数,传递的参数都是java数据类型,而到了JNI层,则需要有所转换。

java层--->JNI层 数据类型转换方式如下:

(1)基本数据类型的转换:数据类型前加上"j"(如boolean--->jboolean、int--->jint)。

(2)引用数据类型的转换:除了java中基本数据类型的数组ClassStringThrowable 外其余所有Java对象的数据类型在JNI中都用jobject表示

看看processFile这个函数:

  java层的processFile函数(有三个参数):

  processFile(String path, String mimeType, MediaScannerClient client);

JNI层中对应的函数(最后三个参数与processFile中对应):
android_media_MediaScanner_processFile( JNIEnv
*env, //见知识点4 jobject thiz, //表示Java层的MediaScanner对象。它表示在哪个MediaScanner对象上调用的processFile。(如果Java层是static函数,那么这个参数将是jclass,表示是在调用哪个java类的静态函数) jstring path, jstring mimeType, jobject client //这三个参数与processFile的参数对应 (java中的MediaScannerClient类型在JNI中对应为jobject).
);

 


知识点4:JNIEnv介绍

JNIEnv是一个与线程相关的代表JNI环境的结构体,实际上就是提供了一些JNI系统函数。通过这些系统函数可以做到:

(1)调用java的函数。

(2)操作jobject对象等很多事情。

 

JavaVM和JNIEnv的关系:

(1)调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调java函数了。

(2)另外,在后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源。

 

 java的引用类型,除少数外,在JNI层都是用jobject表示对象的数据类型,那怎么操作jobject呢?答:通过JNIEnv。

 其实,操作jobject的本质 就是 操作这些对象的成员变量和成员函数。

 操作jobject就两个步骤:

(1)通过jfieldID和jmethodID来表示Java类的成员变量和成员函数。

(2)通过JNIEnv的Call<type>Method方法调用java对象的函数。(其中type对应java的返回值类型

 

 解释(1):JNI规则下,用jfieldID和jmethodID来表示Java类的成员变量和成员函数,可通过JNIEnv的GetFieldID和GetMethodID两个函数得到(它们的第一个参数都是jclass类型,代表Java类)。

下面看一下MS中的例子:

    MyMediaScannerClient(JNIEnv *env, jobject client)......
    {
        //找到android.meida.MediaScanner类在JNI层中对应的jclass实例
        jclass mediaScannerClientInterface =
                env->FindClass(kClassMediaScannerClient);

        if (mediaScannerClientInterface == NULL) {
            ......   
        } else {
            //取出MediaScannerClient类中函数scanFile的jMethodID
            mScanFileMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "scanFile",
                                    "(Ljava/lang/String;JJZZ)V");
            //取出MediaScannerClient类中函数handleStringTag的jMethodID
            mHandleStringTagMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "handleStringTag",
                                    "(Ljava/lang/String;Ljava/lang/String;)V");
            //同上
            mSetMimeTypeMethodID = env->GetMethodID(
                                    mediaScannerClientInterface,
                                    "setMimeType",
                                    "(Ljava/lang/String;)V");
        }
    }

ps:上面代码中,将scanFile、handleStringTag和setMimeType函数的jmethodID保存为 MyMediaScannerClient的成员变量。主要是为了提高效率,不用每次操作jobject都去查询jmethodID和jfiledID

 

解释(2):取出jmethodID后,有什么用呢,该如何使用呢?

其实jmethodID的用处也就是作为JNIEnv的Call<type>Method方法的参数,JNI层将通过Call<type>Method方法调用java对象的函数。

例子:

    virtual status_t scanFile(const char* path, long long lastModified,
            long long fileSize, bool isDirectory, bool noMedia)
    {
        ......
        jstring pathStr;
        if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
            mEnv->ExceptionClear();
            return NO_MEMORY;
        }
        /*
第一个参数是代表MediaScannerClient的jobject对象,
第二个参数是函数scanFile的jmethodID,
后面是java函数中scanFile的参数。
*/ mEnv
->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize, isDirectory, noMedia); mEnv->DeleteLocalRef(pathStr); return checkAndClearExceptionFromCallback(mEnv, "scanFile"); }

 

知识点5:JNI类型签名介绍

1.类型签名的必要性

 Java支持函数重载,因此仅仅根据函数名是无法找到具体函数的,为了解决这个问题,JNI技术引入了类型签名(将参数类型和返回值类型作为一个函数的签名信息)。

2.类型签名的格式

(参数1类型标示;参数2类型表示;......参数n类型标示;)返回值类型标示

例子:(Ljava/lang/String;Landroid/media/MediaScannerClient;)V

 ps:当参数类型是引用类型时,其格式为“L包名;”,其中包名中的“.”换成了“/”

 


知识点6:jstring类型介绍

因为Java中String类型使用频繁,所以JNI规范中单独创建了一个jstring类型来表示Java中的String类型,但jstring类型并没有提供成员函数以便操作。因此还是得依靠JNIEnv。

*调用JNIEnv的NewString(JNIEnv *env, const jchar *unicodeChars,jsize len),可以从Native的字符串得到一个jstring对象。

*调用JNIEnv的NewStringUTF,将根据Native的一个UTF-8字符串得到一个jstring对象。(用到最多)

*上面两个函数将本地字符串转换成了Java的String对象,JNIEnv还提供了GetStringChars函数和GetStringUTFChars函数,用于将Java String对象转换成本地字符串。

*注意,如果在代码中调用了上面几个函数,在做完相关工作后,都需要调用ReleaseStringChars函数或ReleaseStringUTFChars函数来对应地释放资源,否则会导致JVM内存泄露

 


 

知识点7:垃圾回收机制

例子:
static jobject save_thiz = null;
static void android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)
{
    ......
    //保存Java层传入的jobject对象,代表MediaScanner对象
    save_thiz = thiz;
    ......
    return;
}

//假设有地方调用callMediaScanner函数
void callMediaScanner()
{
    //这里操作save_thiz,有问题不?
}

肯定会有问题,因为如果java层的thiz已经被回收,则save_thiz保存的很可能是一个野指针了。

JNI提供了三种类型的引用来解决这个问题:

(1)Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference,它包括函数调用时传入的jobject和在JNI层函数中创建的jobject。Local Reference最大的特点是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。(用得比较多)(操作完后,可以调用DeleteLocalRef方法,这样本地引用的变量就会被立即回收

(2)Global Reference:全局引用。这种对象如果不主动释放,它永远不会被垃圾回收。(用得比较多)

(3)Weak Global Reference:弱全局引用。一种特殊的Global Reference,在运行过程中可能会被垃圾回收。所以在使用它之前,需要调用JNIEnv的IsSameObject判断它是否被回收了。

ps:没有及时回收Local Reference可能是造成进程占用内存过多的一个原因,多加注意!

 


知识点8:JNI中异常处理

JNI中,如果调用JNIEnv的某些函数时出错了,则会产生一个异常,但并不会中断本地函数的执行,只会做一些资源清理的工作。直到从JNI层返回Java层后,虚拟机才会抛出该异常。如果此时又有调用除该出错的JNIEnv的函数外的其他JNIEnv函数,则会导致程序死掉。

JNI曾函数可以在代码中截获和修改这些异常,JNIEnv提供了三个函数:

(1)ExceptionOccured函数,用来判断是否发生异常。

(2)ExceptionClear函数,用来清理当前JNI层中发生的异常。

(3)ThrowNew函数,用来向Java层抛出异常。

 

OK,总结完毕!第二章断断续续看了蛮长时间,凡是开头难,继续加油!