JNI基础学习笔记
2020-05-25 22:47:28 Author: bbs.pediy.com(查看原文) 阅读量:388 收藏

深入理解jni

源码版本:2.3.x
学习JNI实例:MediaScanner
MediaScanner的功能是扫描媒体文件并将它们存储到媒体数据库中,供其他程序使用

java 层的 MediaScanner 分析

--> MediaScanner.java

public class MediaScanner
{
    //加载对应的jni库,库名为libmedia_jni
    //在实际加载中会扩展成libmedia_jni.so,Windows平台则是media_jni.dll
    static {
        System.loadLibrary("media_jni");
        native_init();
    }

    //声明一个native函数,native为Java关键字,表示将由JNI层完成
    private static native final void Native_init();
}

JNI 层的 MediaScanner 分析

--> android_media_MediaScanner.cpp

//这个函数是native_init的JNI层实现
static void android_media_MediaScanner_native_init(JNIEnv *env)
{
    jclass clazz;

    clazz = env->FindClass("android/media/MediaScanner");

    fields.context = env->GetFieldID(clazz, "mNativeContext", "I");
    }
}

但是如何确定 Java 层的 native_init 函数对应的就是 JNI 层的 android_media_MediaScanner_native_init 函数。这就涉及到 JNI 函数的注册问题。

注册JNI函数

所谓 注册 就是将 Java 层声明的 Native 函数与 JNI 层对应的实现函数关联起来。在这个例子中, native_init 函数位于 android.media 包中,全路径名为 android.media.MediaScanner.native_intJNI 层函数的名字是 android_media_MediaScanner_native_initJNI 函数的注册方法分为静态注册动态注册

  1. 静态方法
    静态方法就是根据函数名来寻找对应的 JNI 函数。
    一般来说分为两步:
  • 编写 Java 代码,编译生成 .class 文件。
  • 使用 Java 的工具程序 javah ,如 javah -o output packagename.classnamepackagename.classnameJava 代码编译后的 class 文件,而在生成的 output.h 文件里,声明了对应的 JNI 层函数,只要实现里面的函数即可。
    --> androyid_media_MediaScanner.h::样例文件

      #include <jni.h>   //必须包含这个文件,否则编译不通过  
      #ifndef _Included_android_media_MediaScanner
      #define _Included_android_media_MediaScanner
      #ifdef __cplusplus
      extern "C" {
      #endif
    
      //native_init对应的JNI函数
      //Java层函数名中如果有一个"_",被转换成JNI之后就变成了"_l"。  
      JNIEXPORT void JNICALL Java_android_media_MediaScanner_native_linit(JNIEnv *, jclass);
      }
      #endif
      #endif
    

    缺点:

    • 所有声明了 native 函数的类都需要编译并生成头文件
    • javah 生成的 JNI 层函数名太长
    • 初次调用 native 函数时要根据函数名搜索对应 JNI 函数,影响效率
  1. 动态注册
    JNI 技术中,有 JNINativeMethod 结构体用来记录 native 函数和 JNI 函数的对应关系。

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

    MediaScanner JNI 层是这么做的~
    --> android_media_MediaScanner.cpp

    //定义一个JNINativeMethod数组,其成员就是MS中所有native函数的一一对应关系
    static JNINativeMethod gMethods[] = {
        ······
     {
         "native_init",   //java中native函数的函数名
         "()V",
         (void *)android_media_MediaScanner_native_init  //JNI层对应的函数指针
     },
         ······
    };
    
    //注册JNINativeMethod数组
    int register_android_media_MediaScanner(JNIEnv * env)
    {
        //调用AndroidRuntime的registerNativeMethods函数,第二个参数标明是Java中的哪个类
        return AndroidRuntime::registerNativeMethods(env, "android/media/MediaScanner", gMethods, NELEM(gMethods));
    }
    

    接下来是 registerNativeMethods 的实现
    --> AndroidRuntime.cpp

    int AndroidRuntime::registerNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods)
    {
        //调用jniRegisterNativeMethods函数完成注册
        return jniRegisterNativeMethos(env, className, gMethods, numMethods);
    }
    

    jniRegisterNativeMethodsAndroid 平台为了方便 JNI 使用而提供的帮助函数。
    --> JNIHelp.c

    int jniRegisterNativeMethods(JNIEnv* env, const char* className, const JNINativeMethod* gMethods, int numMethods)
    {
        jclass clazz;
        clazz = (*env)->FindClass(env, className);
    
        //实际上是调用JNIEnv的RegisterNatives函数完成注册的
        if (*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0 ){
            return -1;
        }
        return 0;
    }
    

    真是多重调用...
    android_media_MediaScanner::register_android_media_MediaScanner -> AndroidRuntime::registerNativeMethods -> JNIHelp::jniRegisterNativeMethods
    但说到最后也就 jniRegisterNativeMethods 中的两步

    那么是什么时候完成注册呢?当 Java 层通过 System.loadLibray 加载完 JNI 动态库后,紧接着会查找 JNI_OnLoad 函数,如果有,就调用并进行注册。libmedia_jni.soJNI_OnLoad 函数我们可以在 android_media_MediaPlayer.cpp 中找到。
    --> android_media_MediaPlayer.cpp

     jint JNI_OnLoad(JavaVM* vm, void* reserved)
     {
         //JavaVM -> 虚拟机在JNI层的代表
         //每个Java进程只有一个JavaVM
         JNIEnv* env = NULL;
         jint result = -1;
    
         if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK){
             goto bail;
         }
         //动态注册MediaScanner的JNI函数
         if (register_android_media_MediaScanner(env) < 0){
             goto bail;
         }
    
         return JNI_VERSION_1_4;
     }
    

    数据类型转换

    这里没什么花里胡哨的,就两个表格
    基本数据类型
    引用类型
    可以看到除了 Java 中基本数据类型的数组、ClassStringThrowable外,其余所有的 Java 对象的数据类型在 JNI中用 jobject表示。

    //Java层
    processFile(String path, String mimeType, MediaScannerClient client);
    //JNI层
    android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)
    

JNIEnv

JNIEnv 是一个与线程相关的代表JNI环境的结构体。
JNIEnv内部结构
通过调用JNIEnv的一些JNI系统函数进而可以调用 Java 的函数、操作 jobject 对象等很多事情。

操作 jobject 即是操作该对象的成员变量和成员函数,在 JNI 规则中,用 jfieldIDjmethodID 表示。

jfieldID GetFieldID(jclass clazz, const char *name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char *name, const char *sig);

MS中这样使用它们
--> android_media_MediaScanner.cpp::MyMediaScannerClient构造函数
--> android_media_MediaScanner.cpp::MyMediaScannerClient的scanFile

MyMediaScannerClient(JNIEnv *env, jobject client)....
{
    //先找到android.media.MediaScannerClient类在JNI层中对应的jclass实例
    jclass mediaScannerClientInterface = env->FindClass("android/media/MediaScannerClient");
    //取出MediaScannerClient类中的函数scanFile的jMethodID
    mScanFileMethodID = env->GetMethodID(mediaScannerClientInterface, "scanFile", "(Ljava/lang/String;JJ)V");
}

virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
{
    jstring pathStr;
    if ((pathStr = mEnv->NewStringUTF(path) == NULL) return false;

    /*
        调用JNIEnv的CallVoidMethod函数,注意CallVoidMethod的参数
        第一个代表MediaScannerClient的jobject参数
        第二个代表函数scanFile的jmethodID,后面是Java中的scanFile的参数
    */
    mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);

    mEnv->DeleteLocalRef(pathStr);
    return (!mEnv->ExceptionCheck());
}

JNI类型签名

Java 支持函数重载,即可以定义同名但不同参数的函数。仅仅通过函数名无法找到具体函数。为了解决这个问题,JNI 技术中将参数类型和返回值类型的组合作为了一个函数的签名信息,有了签名信息和函数名,就能很顺利的找到 Java 中的函数。
格式: ( 参数 1 类型标示 参数 2 类型标示 ... 参数 n 类型标示 ) 返回值类型标示
举例: ( Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient; )V

类型标示示意表

垃圾回收

Java 中创建的对象最后由垃圾回收器回收和释放内存。
--> 垃圾回收的例子

static jobject save_thiz = NULL;  //定义一个全局的jobject
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,会有问题吗?
}

这个做法会有问题,因为和 save_thiz 对应的 java 层中的 MediaScanner 很有可能已经被垃圾回收了,即 save_thiz 保存的 jobject 可能是个野指针,如果使用它,后果会很严重。在被引用中被清理的原因是: JNI 层使用 save_thiz = thiz 这种语句是不会增加 jobject 的引用计数的。
为此, JNI 提供了三种类型的引用:

  • Local Reference:本地引用。在 JNI 层函数使用的非全局引用对象都是 Local Reference ,包括函数调用时传入的 jobject 和在 JNI 层函数中创建的 jobjectLocal Reference 最大的特点就是,一旦 JNI 层函数返回,这些 jobject 就可能被垃圾回收。
  • Global Reference:全局引用,这种对象如不主动释放,它永远不会被垃圾回收。
  • Weak Global Reference:软全局引用,在运行过程中可能会被垃圾回收,所以在使用之前,需要调用 JNIEnvIsSameObject 判断它是否被回收了。

--> android_media_MediaScanner.cpp::MyMediaScannerClient构造函数

//使用 Global Reference,记得释放。

MyMediaScannerClient(JNIEnv *env, jobject client)
        :   mEnv(env),
        //调用NewGlobalRef创建一个Global Reference,这样mClient就不用担心被回收了。
            mClient(env->NewGlobalRef(client)),
            mScanFileMethodID(0),
            mHandStringTagMethodID(0),
            mSetMimeTypeMethodID(0)
{
    ......
}
//析构函数
virtual ~MyMediaScannerClient()
{
    mEnv->DeleteGlobalRef(mClient); //调用DeleteGlobalRef释放这个全局引用
}

JNI中的异常处理

调用 JNIEnv 的某些函数出错,会产生异常,但直到返回到 Java 层后才会抛出。异常不会中断本地函数的运行,但是只能做资源清理的工作,如果此时调用其他 JNIEnv 函数,则会导致程序死掉。
--> android_media_MediaScanner.cpp::MyMediaScanner的scanFile函数

virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
{
    jstring pathStr;
    //NewStringUTF调用失败后,直接返回,不能再干别的事情了
    if ((pathStr = nEnv->NewStringUTF([path)) == NULL) return false;
    ......
}

JNIEnv 提供了三个函数帮助在代码中截获和修改这些异常

  • ExceptionOccured函数,用来判断是否发生
  • ExceptionClear函数,用来清理
  • ThrowNew函数,用来向 Java 层抛出异常

[推荐]看雪工具下载站,全新登场!(Android、Web、漏洞分析还未更新)


文章来源: https://bbs.pediy.com/thread-259696.htm
如有侵权请联系:admin#unsafe.sh