Android JNI浅析(2)

上一篇blog我们讲到了JNINativeMethod结构体和AndroidRuntime::registerNativeMethods这个静态函数。我们在回顾以下JNINativeMethod结构体。

  1. typedef struct {  
  2.  const char* name; /*Java 中函数的名字*/  
  3.  const char* signature; /*描述了函数的参数和返回值*/  
  4.  void* fnPtr; /*函数指针,指向 C 函数*/  
  5.  } JNINativeMethod;  

        当VM载入libxxx_jni.so这个库时,就会呼叫JNI_OnLoad()函数。在JNI_OnLoad()中注册本地函数,继续调用到AndroidRuntime::registerNativeMethods(),该函数向VM(即AndroidRuntime)注册gMethods[]数组中包含的本地函数了。AndroidRuntime::registerNativeMethods()起到了以下两个作用:

1,registerNativeMethods()函数使得java空间中的Native函数更加容易的找到对应的本地函数。(通过gMethods[]中的函数指针)

2,可以在执行期间进行本地函数的替换。因为gMethods[]数组是一个<java中函数名字,本地函数指针>的对应表,所以可以在程序的执行过程中,多次呼叫registerNativeMethods()函数来更换本地函数的指针,提高程序的弹性。


函数签名:

       在JNINativeMethod的结构体中,有一个描述函数的参数和返回值的签名字段,它是java中对应函数的签名信息,由参数类型和返回值类型共同组成。这个函数签名信息的作用是什么呢?

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

        JNI规范定义的函数签名信息格式如下:

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

  1. “()V”  
  2. "(II)V"  
  3. “(Ljava/lang/String;Ljava/lang/String)V";  


        实际上这些字符是与函数的参数类型一一对应的。
        “()” 中的字符表示参数,后面的则代表返回值。例如”()V” 就表示 void Func();
        “(II)V” 表示 void Func(int, int);

        值得注意的一点是,当参数类型是引用数据类型时,其格式是“L包名;”其中包名中的“.” 换成“/”,所以在上面的例子中(Ljava/lang/String;Ljava/lang/String;)V 表示 void Func(String,String);

         如果 JAVA 函数位于一个嵌入类,则用$作为类名间的分隔符。

         例如 “(Ljava/lang/String;Landroid/os/FileUtils$FileStatus;)Z”


        具体的对应关系见下面两张图:

 


        数组则以”["开始,用两个字符表示


         以上都是基本数据类型,前面我们解决了JNI函数的注册问题,下面我们来考虑这样一个问题。在java中调用native函数传递的参数是java数据类型,那么这些参数类型到了JNI会变成什么呢? 下面我们引出了一个新的话题——数据类型转换。


数据类型转换:

       在java层调用native函数传递到JNI层的参数,JNI层会做一些特殊处理,我们知道java数据类型分为基本数据类型和引用数据类型两种,JNI层也是区别对待的。下表示出了java数据类型—>native类型的转换。


         其中在java数据类型中,除了java中基本数据类型和数组,Class,String,Throwable,其余所有的java对象的数据类型在JNI中用jobject表示。下面来看一段代码

  1. {"native_invoke",       "(Landroid/os/Parcel;Landroid/os/Parcel;)I",(void *)android_media_MediaPlayer_invoke},  
  1. //java层native_invoke函数有两个参数都是Parcel  
  2. private native final int native_invoke(Parcel request, Parcel reply);  
  3.   
  4. //JNI层对应的函数android_media_MediaPlayer_invoke的最后两个参数与native_invoke的参数对应  
  5. android_media_MediaPlayer_invoke(JNIEnv *env, jobject thiz,  
  6.                                  jobject java_request, jobject java_reply)  


        从上面的代码可以看出来,java中的数据类型Parcel在JNI层对应的数据类型为jobejct,在JNI层的对应函数中,我们看到相对java层的native函数来说,多了两个参数JNIEnv *env ,jobject thiz。其中JNIEnv的定义在前一篇blog我们已经介绍过了。第二个参数jobject代表了java层的MediaPlayer对象,它表示在哪个MediaPlayer对象上调用的native_invoke。如果java层是static函数,那么这个参数将是jclass,表示是在调用那个java Class的静态函数。

        还记得前面我们说过,java层和JNI层应该是可以互相交互,我们通过java层中的native函数可以进入到JNI层,那么JNI层的代码能不能操作java层中函数呢?当然可以,通过JNIEnv


JNIEnv再度解析

         先来看看两个函数原型

  1. <span style="color:#000000;">jfieldID GetFieldID(jclass clazz,const char *name,const char *sig );  
  2. jmethodID GetMethodID(jclass clazz,const char *name,const char *sig);</span>  

         结合前面的知识来看,JNIEnv是一个与线程相关的代表JNI环境的结构体。JNIEnv实际上提供了一些JNI系统函数。通过这些系统函数可以调用java层中的函数或者操作jobect。下面我看一段函数

  1. class MyMediaScannerClient : public MediaScannerClient  
  2. {  
  3. public:  
  4.     MyMediaScannerClient(JNIEnv *env, jobject client)  
  5.         :   mEnv(env),  
  6.             mClient(env->NewGlobalRef(client)),  
  7.             mScanFileMethodID(0),  
  8.             mHandleStringTagMethodID(0),  
  9.             mSetMimeTypeMethodID(0)  
  10.     {  
  11.         jclass mediaScannerClientInterface = env->FindClass("android/media/MediaScannerClient");  
  12.         if (mediaScannerClientInterface == NULL) {  
  13.             fprintf(stderr, "android/media/MediaScannerClient not found\n");  
  14.         }  
  15.         else {  
  16.             mScanFileMethodID = env->GetMethodID(mediaScannerClientInterface, "scanFile",  
  17.                                                      "(Ljava/lang/String;JJ)V");  
  18.             mHandleStringTagMethodID = env->GetMethodID(mediaScannerClientInterface, "handleStringTag",  
  19.                                                      "(Ljava/lang/String;Ljava/lang/String;)V");  
  20.             mSetMimeTypeMethodID = env->GetMethodID(mediaScannerClientInterface, "setMimeType",  
  21.                                                      "(Ljava/lang/String;)V");  
  22.             mAddNoMediaFolderMethodID = env->GetMethodID(mediaScannerClientInterface, "addNoMediaFolder",  
  23.                                                      "(Ljava/lang/String;)V");  
  24.         }  
  25.     }  
  26. ...  
  27.   
  28. // returns true if it succeeded, false if an exception occured in the Java code  
  29.     virtual bool scanFile(const char* path, long long lastModified, long long fileSize)  
  30.     {  
  31.         jstring pathStr;  
  32.         if ((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;  
  33.   
  34.         mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);  
  35.   
  36.         mEnv->DeleteLocalRef(pathStr);  
  37.         return (!mEnv->ExceptionCheck());  
  38.     }  

可以看到上面的代码中,先找到java层中MediaScannerClinet类在JNI层中对应的jclass实例(通过FindClass)然后拿到MediaScannerclient类中所需要用到函数的函数函数id(通过GetMethodID)。接着通过JNIEnv调用CallXXXMethod函数并且把对应的jobject,jMethodID还有对应的参数传递进去,这样的通过CallXXXMethod就完成了JNI层向java层的调用。这里要注意一点的是这里JNI层中调用的方法实际上是java中对象的成员函数,如果要调用static函数可以使用CallStaticXXXMethod。这种机制有利于native层回调java代码完成相应操作。

               上面讲述了如下在JNI层中去调用java层的代码,那么理所当然的应该可以在JNI层中访问或者修改java层中某对象的成员变量的值。我们通过JNIEnv中的GetFieldID()函数来得到java中对象的某个域的id。看下面的具体代码

  1. int register_android_backup_BackupHelperDispatcher(JNIEnv* env)  
  2. {  
  3.     jclass clazz;  
  4.   
  5.     clazz = env->FindClass("java/io/FileDescriptor");  
  6.     LOG_FATAL_IF(clazz == NULL, "Unable to find class java.io.FileDescriptor");  
  7.     s_descriptorField = env->GetFieldID(clazz, "descriptor""I");  
  8.     LOG_FATAL_IF(s_descriptorField == NULL,  
  9.             "Unable to find descriptor field in java.io.FileDescriptor");  
  10.       
  11.     clazz = env->FindClass("android/app/backup/BackupHelperDispatcher$Header");  
  12.     LOG_FATAL_IF(clazz == NULL,  
  13.             "Unable to find class android.app.backup.BackupHelperDispatcher.Header");  
  14.     s_chunkSizeField = env->GetFieldID(clazz, "chunkSize""I");  
  15.     LOG_FATAL_IF(s_chunkSizeField == NULL,  
  16.             "Unable to find chunkSize field in android.app.backup.BackupHelperDispatcher.Header");  
  17.     s_keyPrefixField = env->GetFieldID(clazz, "keyPrefix""Ljava/lang/String;");  
  18.     LOG_FATAL_IF(s_keyPrefixField == NULL,  
  19.             "Unable to find keyPrefix field in android.app.backup.BackupHelperDispatcher.Header");  
  20.       
  21.     return AndroidRuntime::registerNativeMethods(env, "android/app/backup/BackupHelperDispatcher",  
  22.             g_methods, NELEM(g_methods));  
  23. }  
获得jfieldID之后呢,我们就可以在JNI层之间来访问和操作java层的field的值了,方法如下

  1. NativeType Get<type>Field(JNIEnv *env,jobject object,jfieldID fieldID)  
  2.   
  3. void Set<type>Field(JNIEnv *env,jobject object ,jfieldID fieldID,NativeType value)  
注意这里的NativeType值得是jobject,jboolean等等。


        现在我们看到有了JNIEnv,我们可以很轻松的操作jobject所代表的java层中的实际的对象了。

jstring介绍

         之所以要把jstring单独拿出来说正是由于它的特殊性。java中String类型也是一个引用类型,但是JNI中并没有用jobject来与之对应,JNI中单独创建了一个jstring类型来表示java中的String类型。显然java中的String不能和C++中的String等同起来,那么怎么操作jstring呢?方法很多下面看几个简单的方法

1,调用JNIEnv的NewStringUTF将根据Native的一个UTF-8字符串得到一个jstring对象。只有这样才能让一个C++中String在JNI中使用。

2,调用JNIEnv的GetStringChars函数(将得到一个Unicode字符串)和GetStringUTFChars函数(将得到一个UTF-8字符串),他们可以将java String对象转换诚本地字符串。下面我们来看段事例代码。

  1. virtual bool scanFile(const char* path, long long lastModified, long long fileSize)  
  2.     {  
  3.         jstring pathStr;  
  4.         //将char*数组字符串转换诚jstring类型  
  5.         if ((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;  
  6.   
  7.         mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);  
  8.   
  9.         mEnv->DeleteLocalRef(pathStr);  
  10.         return (!mEnv->ExceptionCheck());  
  11.     }  
  12.   
  13.       ....  
  14.       ....  
  15. while (env->CallBooleanMethod(iter, hasNext)) {  
  16.             jobject entry = env->CallObjectMethod(iter, next);  
  17.             jstring key = (jstring) env->CallObjectMethod(entry, getKey);  
  18.             jstring value = (jstring) env->CallObjectMethod(entry, getValue);  
  19.   
  20.             const char* keyStr = env->GetStringUTFChars(key, NULL);  
  21.             ...  
  22.             ...  
GetStringUTFChars()函数将jstring类型转换成一个UTF-8本地字符串,另外如果代码中调用了上面的几个函数,则在做完相关工作后,要调用ReleaseStringChars函数或者ReleaseStringUTFChars函数来释放资源。看下面的代码

  1.             ...  
  2.             ...  
  3.             jstring key = (jstring) env->CallObjectMethod(entry, getKey);  
  4.             jstring value = (jstring) env->CallObjectMethod(entry, getValue);  
  5.   
  6.             const char* keyStr = env->GetStringUTFChars(key, NULL);  
  7.             if (!keyStr) {  // Out of memory  
  8.                 jniThrowException(  
  9.                         env, "java/lang/RuntimeException""Out of memory");  
  10.                 return;  
  11.             }  
  12.   
  13.             const char* valueStr = env->GetStringUTFChars(value, NULL);  
  14.             if (!valueStr) {  // Out of memory  
  15.                 jniThrowException(  
  16.                         env, "java/lang/RuntimeException""Out of memory");  
  17.                 return;  
  18.             }  
  19.   
  20.             headersVector.add(String8(keyStr), String8(valueStr));  
  21.   
  22.             env->DeleteLocalRef(entry);  
  23.             env->ReleaseStringUTFChars(key, keyStr);  
  24.             env->DeleteLocalRef(key);  
  25.             ...  
  26.             ...  
可以看到GetStringUTFChars与下面的Release StringUTFChars对应。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值