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


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

    第二章   深入理解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,总结完毕!第二章断断续续看了蛮长时间,凡是开头难,继续加油!

  • 相关阅读:
    区块链系列教程
    第三章 通过java SDK 实现个性化智能合约的部署与测试
    第一章 区块链系列 联盟链FISCO BCOS 底层搭建
    ABP 框架 数据库底层迁移 Mysql 集群
    ABP 框架代码批量生成器
    基于百度理解与交互技术实现机器问答
    微软人工智能和对话平台--知识商城体验
    基于百度AI实现 车牌识别
    最近整理AI相关感想
    百度OCR文字识别-身份证识别
  • 原文地址:https://www.cnblogs.com/chenbin7/p/2670852.html
Copyright © 2020-2023  润新知