Android JNI 分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012417380/article/details/78053507

本文我们将分析Android的jni机制。

一、JNI 概述

JNI 的全称是 Java Native Interface, 中文名称 “Java本地调用接口”, JNI标准是Java平台的一部分,它允许Java代码能够和其它语言写的代码进行交互。出现JNI技术有以下原因:

  • Java语言平台无关,但执行Java语言的虚拟机却是用Native语言写的,与平台相关。出现JNI技术可以向Java层屏蔽平台相关的具体实现细节。
  • 编译后的Native语言执行效率更高,将用navtive语言编写的库让java层调用,执行效率更高。

从本质上看,Android平台是由linux操作系统和Dalvik Java虚拟机构成,Dakvik 提供了一个标准的支持JNI调用的Java虚拟机环境。JNI是连接Java层和C/C++部分的纽带。

Android 4.4.4_r1源码中,主要的JNI代码放在frameworks\base\core\jni目录下,查看该目录下的Android.mk 文件,可以发现该目录下的文件被编译成了了libandroid_runtime.so

二、MediaScanner分析

Media系统使用jni技术,我们可以分析MediaScanner来学习JNI相关技术.涉及的源码有:

frameworks\base\core\jni\AndroidRuntime.cpp
frameworks\base\media\java\android\media\android_media_MediaScanner.cpp
frameworks\base\media\jni\android_media_MediaPlayer.cpp
frameworks\base\media\java\android\media\MediaPlayer.java
libnativehelper\JNIHelp.cpp

MediaScannerJNI相关部分是:

Java(MediaScanner)<=>JNI(libmedia_jni.so)<=>Native(libmedia.so)

2.1 Java层分析

我们首先分析frameworks\base\media\java\android\media\MediaScanner.java类。

public class MediaScanner{

    static {
        System.loadLibrary("media_jni");
        native_init();
    }
    ......
    private static native final void native_init();
    private native void processFile(String path, String mimeType, MediaScannerClient client);
    ......
}

可以看到MediaScanner类中先加载media_jni.so共享库,然后调用native_init方法进行初始化。

总结:

1.Java层使用Native层的函数前,需要先加载动态库,通常加载时机是在类的 static 代码块中,调用System.loadLibrary 方法实现,loadLibray 的参数media_jni是动态库的名称,不用加后缀。

2.Java中的函数由native层实现时,需要在方法前添加native关键字

2.2 JNI层分析

Android的应用层的类都是以Java语言写成,Java类编译为Dex形式的字节码,由Dalvik虚拟机来执行实现。如果Java类需要与C组件进行交互,则由VM载入C组件。VM扮演着桥梁的角色,使Java语言跟C语言能够相互沟通。

MediaScanner的JNI层实现在 frameworks\base\media\jni\android_media_MediaScanner.cpp文件中


// native_init 的JNI层实现。
// This function gets a field ID, which in turn causes class initialization.
// It is called from a static block in MediaScanner, which won't run until the
// first time an instance of this class is used.
static void
android_media_MediaScanner_native_init(JNIEnv *env)
{
    ALOGV("native_init");
    jclass clazz = env->FindClass(kClassMediaScanner);
    if (clazz == NULL) {
        return;
    }

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


//processFile的JNI层实现。

static void
android_media_MediaScanner_processFile(
        JNIEnv *env, jobject thiz, jstring path,
        jstring mimeType, jobject client)
{
    ALOGV("processFile");

    // Lock already hold by processDirectory
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "No scanner available");
        return;
    }

    if (path == NULL) {
        jniThrowException(env, kIllegalArgumentException, NULL);
        return;
    }

    const char *pathStr = env->GetStringUTFChars(path, NULL);
    if (pathStr == NULL) {  // Out of memory
        return;
    }

    const char *mimeTypeStr =
        (mimeType ? env->GetStringUTFChars(mimeType, NULL) : NULL);
    if (mimeType && mimeTypeStr == NULL) {  // Out of memory
        // ReleaseStringUTFChars can be called with an exception pending.
        env->ReleaseStringUTFChars(path, pathStr);
        return;
    }

    MyMediaScannerClient myClient(env, client);
    MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
    if (result == MEDIA_SCAN_RESULT_ERROR) {
        ALOGE("An error occurred while scanning file '%s'.", pathStr);
    }
    env->ReleaseStringUTFChars(path, pathStr);
    if (mimeType) {
        env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
    }
}

2.2.1 注册JNI函数

Java层的函数名跟Native层的函数名有怎样的对应关系?

通过上面的分析我们知道,native_init函数对应的JNI函数是android_media_MediaScanner_native_init
MediaScanner 类在android.media包中,native_init的全路径名为android.media.MediaScanner.native_init,在Native语言中,.具有特殊的含义,我们将·_换成.即可得到Java层函数对应的Native函数。

Java层函数与Native层的函数联系起来的方法有两种,一种是静态注册、另一种是动态注册。

  • 静态注册

静态注册流程如下:

1.编写Java代码,生成.class文件

2.使用javah工具,生成packagename_class.h文件

 javah -classpath bin/classes -d jni com.example.testndk.MainActivity

packagename_class.h文件中,声明了java类中native关键字方法对应的本地方法,将它们实现即可。

头文件一般使用packagename_class.h的形式。而生成的函数名为Java_ackagename_class_method

  • 动态注册

Java的Native函数与JNI函数是一一对应的关系,JNI技术中,使用 JNINativeMethod 结构体来记录这种一一对应关系。


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

JNI中如何使用这种对应关系,查看frameworks\base\media\jni\android_media_MediaScanner.cpp可知:

...
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
    },

    {
        "setLocale",
        "(Ljava/lang/String;)V",
        (void *)android_media_MediaScanner_setLocale
    },

    {
        "extractAlbumArt",
        "(Ljava/io/FileDescriptor;)[B",
        (void *)android_media_MediaScanner_extractAlbumArt
    },

    {
        "native_init",
        "()V",
        (void *)android_media_MediaScanner_native_init
    },

    {
        "native_setup",
        "()V",
        (void *)android_media_MediaScanner_native_setup
    },

    {
        "native_finalize",
        "()V",
        (void *)android_media_MediaScanner_native_finalize
    },
};


// 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,
                kClassMediaScanner, gMethods, NELEM(gMethods));
}

AndroidRunTime类提供了一个registerNativeMethods函数来完成注册

#frameworks\base\core\jni\AndroidRuntime.cpp
/*
 * Register native methods using JNI.
 */
/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

其中jniRegisterNativeMethods方法是Android平台为了方便JNI使用而提供的一个帮助函数,在JNIHelp.cpp类中。

#libnativehelper\JNIHelp.cpp

extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    JNIEnv* e = reinterpret_cast<JNIEnv*>(env);

    ALOGV("Registering %s's %d native methods...", className, numMethods);

    scoped_local_ref<jclass> c(env, findClass(env, className));
    if (c.get() == NULL) {
        char* msg;
        asprintf(&msg, "Native registration unable to find class '%s'; aborting...", className);
        e->FatalError(msg);
    }
     //实际上是调用JNIEnv 的RegisterNatives
    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
        char* msg;
        asprintf(&msg, "RegisterNatives failed for '%s'; aborting...", className);
        e->FatalError(msg);
    }

    return 0;
}

事实上完成JNI函数的注册,只需要两个函数即可

//env指向一个JNIEnv的结构体,classname为对应的Java类名,由于JNInativeMethod中使用的函数名并非全路径名 ,所以要指明是那个类。
 clazz = (*env)->FindClass(env, className);  
if ((*env)->RegisterNatives(env, clazz, gMethods,numMethods) < 0){  

当Java层通过System.loadLibrary加载完JNI库后,就会查找JNI_OnLoad方法,如果有的话,就调用它。所以动态注册在JNI_OnLoad方法中完成。

libmedia_jni.so的JNI_OnLoad函数在android_media_MediaPlayer.cpp

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

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


    ......


    if (register_android_media_MediaScanner(env) < 0) {
        ALOGE("ERROR: MediaScanner native registration failed\n");
        goto bail;
    }

    ......

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

bail:
    return result;
}

2.2.2数据类型的转换

Java中调用native函数传递的参数类型是Java数据类型,参数类型到了JNI层会变成其他类型数据类型。Java数据类型分为基本数据和引用数据类型两种,JNI层也是区别对待这两者。

1.基本数据类型的转换

这里写图片描述

2.引用数据类型的转换

这里写图片描述

2.2.3 JNIEnv 介绍

JNIEnv是一个与线程相关的代表JNI环境的结构体。线程A有一个JNIEnv,线程B有一个JNIEnv。由于线程相关,所以不能在线程B中使用线程A中的结构体。
正常情况下,Native函数使用传进来JNIEnv参数不会有错。但是当后台线程收到一个网络消息,又需要由Native层函数回调Java层函数时,JNI如何获取?我们不能保存另外一个线程的JNIEnv结构体,然后把他们放到后台线程中来用。

在上面的JNI_OnLoad函数中,第一个参数是JavaVM对象,它是虚拟机在JNI层的代表。

//全进程只有一个JavaVM对象,所以可以保存,并且在任何地方使用都没有问题
jint JNI_OnLoad(JavaVM* vm, void* reserved);

在整个JNI_Onload进程中,有且只有一个Java VM对象,可以保存并在任何地方使用它,通过Java VM对象,可以获取到JNIEnv对象:

方法1:(*jvm)->AttachCurrentThread(jvm,(void**)&env,NULL);

方法2:(*jvm)->GetEnv(jvm,(void**)&env,JNI_VERSION_1_4);

2.2.4 JNIEnv 操作jobject对象

Java中的引用类型除了少数几个外,其余的用jobject对象表示。操作jobject对象的本质就应当是操作这些对象的成员变量和成员函数。

jfiledIDjmethodID

我们可以通过jfiledIDjmethodID来表示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为这个函数和变量的签名信息。

#android_media_MediaScanner.cpp::MyMediaScannerClient类的构造函数 :



static const char* const kClassMediaScannerClient =
        "android/media/MediaScannerClient";


 MyMediaScannerClient(JNIEnv *env, jobject client)
        :   mEnv(env),
            mClient(env->NewGlobalRef(client)),
            mScanFileMethodID(0),
            mHandleStringTagMethodID(0),
            mSetMimeTypeMethodID(0)
    {
        ALOGV("MyMediaScannerClient constructor");
        jclass
       //先找到android.medai.MediaScannerClient类在JNI层中对应的jclass实例。 mediaScannerClientInterface =
                env->FindClass(kClassMediaScannerClient);

        if (mediaScannerClientInterface == NULL) {
            ALOGE("Class %s not found", kClassMediaScannerClient);
        } 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");
        }
    }

如果每次操作jobject前都去查询jmethodID或jfieldID,那么将会影响程序运行的效率,所以我们在初始化的时候可以取出这些ID并保存起来以供后续使用。

2. 使用jfieldIDjmethodID

再看一个例子,其代码如下所示:

#android_media_MediaScanner.cpp::MyMediaScannerClient类的scanFile函数 :

virtual status_t scanFile(const char* path, long long lastModified,
            long long fileSize, bool isDirectory, bool noMedia)
    {
        ALOGV("scanFile: path(%s), time(%lld), size(%lld) and isDir(%d)",
            path, lastModified, fileSize, isDirectory);

        jstring pathStr;
        if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
            mEnv->ExceptionClear();
            return NO_MEMORY;
        }
       /*
        *调用JNIenv的`CallVoidMehod`函数,注意CallVoidMethod的参数 ;第一个是代表MediaScannerClient的jobject对象。第二个参数是函数scanFile的jmethodID,后面是Java中scanFile的参数。
       */
        mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
                fileSize, isDirectory, noMedia);

        mEnv->DeleteLocalRef(pathStr);
        return checkAndClearExceptionFromCallback(mEnv, "scanFile");
    }

通过JNIEnv输出CallVoidMethod,再把jobjectjMethodID和对应的参数传进去,JNI层就能够调用Java对象的函数。

JNIEnv输出一系列类似CallVoidMethod函数,形式如下:

NativeType Call<type>Method(JNIEnve * env, jobject obj,jmethodID methodID,... )

type对应Java函数的返回值类型。

上面是针对非static函数,如果想调用Java中的static函数,则用CallStatic<Type>Method系列函数。

通过JNIEnv操作jobject的成员函数,通过jfieldID来进行操作:


NativeType  Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)

void Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)

2.2.5 jstring介绍

Java中String类的存储类型为Unicode字符。JNI中用jstring类型来 表示Java中的Stirng
下面是关于JNIEnv中关于Stirng

 jstring     (*NewString)(JNIEnv*, const jchar*, jsize);

 jstring     (*NewStringUTF)(JNIEnv*, const char*);


 //得到本地`Unicode`字符
 const jchar* (*GetStringChars)(JNIEnv*, jstring, jboolean*);
 //得到本地`UTF-8`字符
 const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);

 //释放本地字符串
 void        (*ReleaseStringChars)(JNIEnv*, jstring, const jchar*);
 void        (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);


  jsize       (*GetStringLength)(JNIEnv*, jstring);
  jsize       (*GetStringUTFLength)(JNIEnv*, jstring);

释放本地字符串示例:

static void
android_media_MediaScanner_processDirectory(
        JNIEnv *env, jobject thiz, jstring path, jobject client)
{
    ALOGV("processDirectory");
    MediaScanner *mp = getNativeScanner_l(env, thiz);

    ...
    //调用JNIEnv 的GetStringUTFChars得到本地字符串pathStr
    const char *pathStr = env->GetStringUTFChars(path, NULL);

   ...
   //使用完后,必须调用`ReleaseStringUTFChars`释放资源
    env->ReleaseStringUTFChars(path, pathStr);
}

2.2.6 JNI类型签名介绍

Java中函数签名信息由参数类型和返回值类型共同组成。由于Java支持函数重载,函数名可以相同,而参数不同。
JNI中将参数类型和返回值类型的组合作为了一个函数的签名,来寻找函数的签名。
类型标志:
这里写图片描述

函数签名小例子:
这里写图片描述

参考文章:
http://blog.csdn.net/omnispace/article/details/51773318#t0
《深入理解Android 卷 I》

猜你喜欢

转载自blog.csdn.net/u012417380/article/details/78053507
今日推荐