Android深入理解JNI(二)类型转换、方法签名和JNIEnv

刘望舒
• 阅读 2785
  • Android框架层
  • Android深入理解JNI
  • Android框架层

本文首发于微信公众号「刘望舒」

前言

上一篇文章介绍了JNI的基本原理和注册,这一篇接着带领大家来学习JNI的数据类型转换、方法签名和JNIEnv。

1.数据类型的转换

首先给出上一篇文章中android_media_MediaRecorder.cpp中的android_media_MediaRecorder_start方法: frameworks/base/media/jni/android_media_MediaRecorder.cpp

static void
android_media_MediaRecorder_start(JNIEnv *env, jobject thiz)
{
    ALOGV("start");
    sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
    process_media_recorder_call(env, mr->start(), "java/lang/RuntimeException", "start failed.");
}

android_media_MediaRecorder_start方法有一个参数为jobject类型,它是JNI层的数据类型,Java的数据类型到了JNI层就需要转换为JNI层的数据类型。Java的数据类型分为基本数据类型和引用数据类型,JNI层对于这两种类型也做了区分,我们先来查看基本数据类型的转换。

1.1 基本数据类型的转换

Java Native Signature
byte jbyte B
char jchar C
double jdouble D
float jfloat F
int jint I
short jshort S
long jlong J
boolean jboolean Z
void void V

从上表可以可看出,基本数据类型转换,除了void,其他的数据类型只需要在前面加上“j”就可以了。第三列的Signature 代表签名格式,后文会介绍它。接着来看引用数据类型的转换。

1.2 引用数据类型的转换

Java Native Signature
所有对象 jobject L+classname +;
Class jclass Ljava/lang/Class;
String jstring Ljava/lang/String;
Throwable jthrowable Ljava/lang/Throwable;
Object[] jobjectArray [L+classname +;
byte[] jbyteArray [B
char[] jcharArray [C
double[] jdoubleArray [D
float[] jfloatArray [F
int[] jintArray [I
short[] jshortArray [S
long[] jlongArray [J
boolean[] jbooleanArray [Z

从上表可一看出,数组的JNI层数据类型需要以“Array”结尾,签名格式的开头都会有“[”。除了数组以外,其他的引用数据类型的签名格式都会以“;”结尾。 另外,引用数据类型还具有继承关系,如下所示:

Android深入理解JNI(二)类型转换、方法签名和JNIEnv

再来列举MediaRecorder框架的Java方法: frameworks/base/media/java/android/media/MediaRecorder.java

   private native void _setOutputFile(FileDescriptor fd, long offset, long length)
        throws IllegalStateException, IOException

_setOutputFile方法对应的JNI层的方法为: frameworks/base/media/jni/android_media_MediaRecorder.cpp

static void
android_media_MediaRecorder_setOutputFileFD(JNIEnv *env, jobject thiz, jobject fileDescriptor, jlong offset, jlong length)
{
  ...
}

对比这两个方法可以看到,FileDescriptor类型转换为了jobject类型 ,long类型转换为了jlong类型。

2.方法签名

前面表格已经列举了数据类型的签名格式,方法签名就由签名格式组成,那么,方法签名有什么作用呢?我们看下面的代码。 frameworks/base/media/jni/android_media_MediaRecorder.cpp

static const JNINativeMethod gMethods[] = {
  ...
    {"native_init",       "()V",        (void *)android_media_MediaRecorder_native_init},
    {"native_setup",      "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V",
    (void *)android_media_MediaRecorder_native_setup},
   ...
};

gMethods数组中存储的是MediaRecorder的Native方法与JNI层方法的对应关系, 其中"()V"和 "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V"就是方法签名。我们知道Java是有重载方法的,可以定义方法名相同,但参数不同的方法,正因为如此,在JNI中仅仅通过方法名是无法找到 Java中的具体方法的,JNI为了解决这一问题就将参数类型和返回值类型组合在一起作为方法签名。通过方法签名和方法名就可以找到对应的Java方法。 JNI的方法签名的格式为:

(参数签名格式...)返回值签名格式

拿上面gMethods数组的native_setup方法举例,他在Java中是如下定义的:

private native final void native_setup(Object mediarecorder_this,
        String clientName, String opPackageName) throws IllegalStateException;

它在JNI中的方法签名为:

(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V"

参照本文第一节给出的类型转换表格,native_setup方法的第一个参数的签名为“Ljava/lang/Object;”,后两个参数的签名为“Ljava/lang/String;”,返回值类型void 的签名为“V”,组合起来就是上面的方法签名。

如果我们每次编写JNI时都要写方法签名,也会是一件比较头疼的事,幸好Java提供了javap命令来自动生成方法签名。我们先写一个简单的MediaRecorder.java包含上面的native_setup方法:

public class MediaRecorder {
    static {
        System.loadLibrary("media_jni");
        native_init();
    }
    private static native final void native_init();
    private native final void native_setup(Object mediarecorder_this,
        String clientName, String opPackageName) throws IllegalStateException;
}

这个文件的在我的本地地址为D:/Android/MediaRecorder.java,接着执行如下命令:

javac D:/Android/MediaRecorder.java

执行命令后会生成MediaRecorder.class文件,最后使用javap命令:

javap -s -p D:/Android/MediaRecorder.class

其中s 表示输出内部类型签名,p表示打印出所有的方法和成员(默认打印public成员),最终会在cmd中的打印结果如下:

Android深入理解JNI(二)类型转换、方法签名和JNIEnv

可以很清晰的看到输出的native_setup方法的签名和此前给出的一致。

3.JNIEnv

JNIEnv 是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递,因此,不同线程的JNIEnv是彼此独立的,JNIEnv的主要作用有两点: 1.调用Java的方法。 2.操作Java(获取Java中的变量和对象等等)。

先来看JNIEnv的定义,如下所示。 libnativehelper/include/nativehelper/jni.h

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;//C++中JNIEnv的类型 
typedef _JavaVM JavaVM; 
#else
typedef const struct JNINativeInterface* JNIEnv;//C中JNIEnv的类型  
typedef const struct JNIInvokeInterface* JavaVM;
#endif

这里使用预定义宏__cplusplus来区分C和C++两种代码,如果定义了__cplusplus,则是C++代码中的定义,否则就是C代码中的定义。 在这里我们也看到了JavaVM,它是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此,该进程的所有线程都可以使用这个JavaVM。通过JavaVM的AttachCurrentThread函数可以获取这个线程的JNIEnv,这样就可以在不同的线程中调用Java方法了。还要记得在使用AttachCurrentThread函数的线程退出前,务必要调用DetachCurrentThread函数来释放资源。

jfieldID和jmethodID

在JNI中用jfieldID和jmethodID来代表Java类中的成员变量和方法,可以通过JNIEnv的下面两个方法来分别得到:

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

其中,jclass代表Java类,name代表成员方法或者成员变量的名字,sig为这个方法和变量的签名。 我们来查看MediaRecorder框架的JNI层是如何使用上述的两个方法的,如下所示。 frameworks/base/media/jni/android_media_MediaRecorder.cpp

static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
    jclass clazz;
    clazz = env->FindClass("android/media/MediaRecorder");//1
    if (clazz == NULL) {
        return;
    }
    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");//2
    if (fields.context == NULL) {
        return;
    }
    fields.surface = env->GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");//3
    if (fields.surface == NULL) {
        return;
    }
    jclass surface = env->FindClass("android/view/Surface");
    if (surface == NULL) {
        return;
    }
    fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
                                               "(Ljava/lang/Object;IIILjava/lang/Object;)V");//4
    if (fields.post_event == NULL) {
        return;
    }
}

注释1处,通过FindClass来找到Java层的MediaRecorder的Class对象,并赋值给jclass类型的变量clazz,因此,clazz就是Java层的MediaRecorder在JNI层的代表。注释2和注释3处的代码用来找到Java层的MediaRecorder中名为mNativeContext和mSurface的成员变量,并分别赋值给context和surface。注释4出获取Java层的MediaRecorder中名为postEventFromNative的静态方法,并赋值给post_event。其中fields的定义为:

struct fields_t {
    jfieldID    context;
    jfieldID    surface;
    jmethodID   post_event;
};
static fields_t fields;

将这些成员变量和方法赋值给jfieldID和jmethodID类型的变量主要是为了效率考虑,如果每次调用相关方法时都要进行查询方法和变量,显然会效率很低,因此在MediaRecorder框架JNI层的初始化方法android_media_MediaRecorder_native_init中将这些jfieldID和jmethodID类型的变量保存起来,以供后续使用。

使用jfieldID和jmethodID

我们保存了jfieldID和jmethodID类型的变量,接着怎么使用它们呢,如下所示。 frameworks/base/media/jni/android_media_MediaRecorder.cpp

void JNIMediaRecorderListener::notify(int msg, int ext1, int ext2)
{
    ALOGV("JNIMediaRecorderListener::notify");

    JNIEnv *env = AndroidRuntime::getJNIEnv();
    env->CallStaticVoidMethod(mClass, fields.post_event, mObject, msg, ext1, ext2, NULL);//1
}

在注释1处调用了JNIEnv的CallStaticVoidMethod函数,其中就传入了fields.post_event,从上面我们得知,它其实是保存了Java层MediaRecorder的静态方法postEventFromNative: frameworks/base/media/java/android/media/MediaRecorder.java

private static void postEventFromNative(Object mediarecorder_ref,
                                        int what, int arg1, int arg2, Object obj)
{
    MediaRecorder mr = (MediaRecorder)((WeakReference)mediarecorder_ref).get();
    if (mr == null) {
        return;
    }
    if (mr.mEventHandler != null) {
        Message m = mr.mEventHandler.obtainMessage(what, arg1, arg2, obj);
        mr.mEventHandler.sendMessage(m);
    }
}

这样我们就能在JNI层中访问Java的静态方法了。同理,如果想要访问Java的方法则可以使用JNIEnv的CallVoidMethod函数。 上面的例子是使用了jmethodID,接着来查看jfieldID: frameworks/base/media/jni/android_media_MediaRecorder.cpp

static void
android_media_MediaRecorder_prepare(JNIEnv *env, jobject thiz)
{
    ALOGV("prepare");
    sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
    jobject surface = env->GetObjectField(thiz, fields.surface);//1
    if (surface != NULL) {
        const sp<Surface> native_surface = get_surface(env, surface);
       ...
    }
    process_media_recorder_call(env, mr->prepare(), "java/io/IOException", "prepare failed.");

在注释1处调用了JNIEnv的GetObjectField函数,参数中的fields.surface用来保存Java层MediaRecorde中的成员变量mSurface,mSurface的类型为Surface,这样通过GetObjectField函数就得到了mSurface在JNI层中对应的jobject类型变量surface 。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
刘望舒 刘望舒
3年前
Android深入理解JNI(一)JNI原理与静态、动态注册
Android框架层Android深入理解JNIAndroid框架层本文首发于微信公众号「刘望舒」前言JNI不仅仅在NDK开发中应用,它更是Android系统中Java与Native交互的桥梁,不理解JNI的话,你就只能停留在JavaFramework层。这一个系列我们来一起深入学习JNI。<!more1.JNI概述Android系统按语言来划分的
刘望舒 刘望舒
3年前
Android解析WindowManager(三)Window的添加过程
Android框架层Android系统服务WindowManagercategories:Android框架层本文首发于微信公众号「刘望舒」前言在此前的系列文章中我们学习了WindowManager体系和Window的属性,这一篇我们接着来讲Window的添加过程。建议阅读此篇文章前先阅读本系列的前两篇文章。<!more1.概述WindowMana
刘望舒 刘望舒
3年前
Android包管理机制(二)PackageInstaller安装APK
Android框架层Android包管理机制Android框架层本文首发于微信公众号「刘望舒」前言在本系列上一篇文章中我们学习了PackageInstaller是如何初始化的,这一篇文章我们接着学习PackageInstaller是如何安装APK的。本系列文章的源码基于Android8.0。1.PackageInstaller中的处理紧接着上一篇的内
刘望舒 刘望舒
3年前
Android深入理解Context(二)Activity和Service的Context创建过程
Android框架层Android深入理解Contextcategories:Android框架层本文首发于微信公众号「刘望舒」前言上一篇文章我们学习了Context关联类和ApplicationContext的创建过程,这一篇我们接着来学习Activity和Service的Context创建过程。需要注意的是,本篇的知识点会和深入理解四大组件系列的
刘望舒 刘望舒
3年前
Android输入系统(二)IMS的启动过程和输入事件的处理
Android框架层Android输入系统Android框架层本文首发于微信公众号「刘望舒」基于Android8.1前言在上一篇文章中,我们学习了IMS的诞生(创建),IMS创建后还会进行启动,这篇文章我们来学习IMS的启动过程和输入事件的处理。1.IMS的启动过程IMS的创建在SystemServer的startOtherServices方法中,
刘望舒 刘望舒
3年前
Android解析ActivityManagerService(二)ActivityTask和Activity栈管理
Android框架层Android系统服务ActivityManagerServiceAndroid框架层本文首发于微信公众号「刘望舒」前言关于AMS,原计划是只写一篇文章来介绍,但是AMS功能繁多,一篇文章的篇幅远远不够。这一篇我们接着来学习与AMS相关的ActivityTask和Activity栈管理。1.ActivityStackActivi
刘望舒 刘望舒
3年前
Android解析WindowManager(二)Window的属性
Android框架层Android系统服务WindowManagercategories:Android框架层本文首发于微信公众号「刘望舒」前言在上一篇文章我们学习了WindowManager体系,了解了Window和WindowManager之间的关系,这一篇我们接着来学习Window的属性。<!more1.概述上一篇文章中我们讲过了Window
刘望舒 刘望舒
3年前
Android深入四大组件(五)Content Provider的启动过程
Android框架层Android深入四大组件categories:Android框架层本文首发于微信公众号「刘望舒」前言ContentProvider做为四大组件之一,通常情况下并没有其他的组件使用频繁,但这不能作为我们不去深入学习它的理由。关于ContentProvider一篇文章是写不完的,这一篇文章先来介绍它的启动过程。<!more1.q
刘望舒 刘望舒
3年前
Android深入四大组件(二)Service的启动过程
Android框架层Android深入四大组件categories:Android框架层本文首发于微信公众号「刘望舒」前言此前我用较长的篇幅来介绍Android应用程序的启动过程(根Activity的启动过程),这一篇我们接着来分析Service的启动过程。建议阅读此篇文章前,请先阅读和这两篇文章。<!more1.ContextImpl到Activi