当前位置:   article > 正文

Android开发中JNI的全面分析_android jni开发

android jni开发


前言

JNI (Java Native Interface,Java本地接口)是Java平台上定义的一套标准的本地编程接口。JNI允许Java代码与本地代码(如C/C++实现的、依赖于特定硬件和操作系统的代码)互操作,即Java代码可以调用本地代码,本地代码也可以调用Java代码。通过JNI调用本地代码,可以实现Java语言所不能实现的功能。在Android平台上,Dalvik虚拟机会实现JNI定义的接口。

总结:JNI是java与本地Native代码的桥梁


一、JNI 在Android 系统中所处的位置

Android采用分层的体系结构:上层的应用层和应用框架层主要使用Java语言开发;下层则运行一个Linux内核,并在内核之上集成了各种核心库和第三方库,以提供系统运行所需的服务,这部分是用C和C++语言开发。连接这两部分的纽带就是JNI。

在这里插入图片描述
从图中可以看出,JNI可以直接调用本地代码库,并可以通过Dalvik虚拟机实现与应用层和应用框架层之间的交互。Android JNI部分的代码主要位于Android体系结构中的上面两层:
□应用层:采用NDK开发,主要使用标准JNI编程模型实现。
□应用框架层:Android定义了一套JNI编程模型,使用函数注册方式弥补了标准JNI编程模型的不足。
Android 应用框架层JNI部按照模块组织,不同的模块将被编译为不同的共享库,分别为上层提供不同的服务。
NDK与JNI的区别:NDK是为便于开发基于JNI的应用而提供的一套开发和编译工具集;而JNI则是一套编程接口,可以运用在应用层,也可以运用在应用框架层,以实现Java代码与本地代码的互操作。

JNI编程模型的结构十分清晰,可以概括为以下三个步骤:

  1. Java层声明Native方法。
  2. JNI层实现Java层声明的Native方法,在JNI层可以调用底层库或者回调Java层方法。这部分将被编译为动态库(SO文件)供系统加载。
  3. 加载JNI层代码编译后生成的共享库。

二、JNI框架层实例分析

在Android应用程序开发中,一般是调用应用框架层的android.util.Log.java 提供的Java 接口来使用日志系统。比如我们会写如下代码输出日志:

Log.d(TAG,"debug 1og");
  • 1

这个Java接口其实是通过JNI调用系统运行库(即本地库)并最终调用内核驱动程序Logger把Log写到内核空间中的。在Android中,Log系统十分通用,而且其JNI结构非常简洁,那今天我们就以它为例子分析一下JNI的具体调用流程。

1.Log 系统Java层分析

首先看Log系统Java层部分。打开Log.java文件,可以看到这里面只定义了isLoggable 和println_native两个Native方法,代码如下(示例):

package android.util; 
public final class Log ( 
......

public static int d(String tag,String msg)(
//使用Native方法打印日志.LOG_ID_MAIN表示日志ID,有4种:main、radio.events、system 
	return println_native(LOG_ID_MAIN, DEBUG, tag, msg);
}
//声明Native方法isLoggable
public static native boolean isLoggable(String tag, int level); 

......

//声明Native方法printin_native
/**@hide*/ 
public static native int printin_native(int bufID,int priority, String tag, String msg); 
)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

Java层需要做的就这么简单。只需要声明方法为native而无需实现,就可以直接调用,不会出现任何编译错误。

2.Log 系统的JNI层

JNI层是实现Java层方法最关键的部分。对于Log类,其对应的JNI文件是 android_util_Log.cpp。代码如下(示例):

#include "jni.h” //符合JNI规范的头文件,必须包含进来#include "JNIHelp.h"
//Android为更好地支持JNI提供的头文件 
#include "utils/misc.h"
#include "android_runtime/AndroidRuntime.h" 

/*这里便是Java层声明的isLoggable方法的实现代码。
 *JNI方法增加了JNIEnv和jobject两个参数,其余参数和返回值只是将Java参数映射成JNI
 *的数据类型,然后通过调用本地库和JNIEnv提供的JNI函数处理数据,最后返回给Java层*/

static jboolean isLoggable(const char* tag, jint level) {
    return __android_log_is_loggable(level, tag, ANDROID_LOG_INFO);
}

static jboolean android_util_Log_isLoggable(JNIEnv* env, jobject clazz, jstring tag, jint level)
{
    if (tag == NULL) {
        return false;
    }
	//这里调用了JNI函数
    const char* chars = env->GetStringUTFChars(tag, NULL);
    if (!chars) {
        return false;
    }

    jboolean result = false;
    if ((strlen(chars)+sizeof(LOG_NAMESPACE)) > PROPERTY_KEY_MAX) {
        char buf2[200];
        snprintf(buf2, sizeof(buf2), "Log tag \"%s\" exceeds limit of %zu characters\n",
                chars, PROPERTY_KEY_MAX - sizeof(LOG_NAMESPACE));

        jniThrowException(env, "java/lang/IllegalArgumentException", buf2);
    } else {
    	//这里调用了本地库函数
        result = isLoggable(chars, level);
    }

    env->ReleaseStringUTFChars(tag, chars);
    return result;
}
//以下是Java层声明的printin_Native方法的实现代码
static jint android_util_Log_println_native(JNIEnv* env, jobject clazz,
        jint bufID, jint priority, jstring tagObj, jstring msgObj)
{
    const char* tag = NULL;
    const char* msg = NULL;

    if (msgObj == NULL) {
        jniThrowNullPointerException(env, "println needs a message");
        return -1;
    }

    if (bufID < 0 || bufID >= LOG_ID_MAX) {
        jniThrowNullPointerException(env, "bad bufID");
        return -1;
    }

    if (tagObj != NULL)
        tag = env->GetStringUTFChars(tagObj, NULL);//这里调用了JNI函数
    msg = env->GetStringUTFChars(msgObj, NULL);

    int res = __android_log_buf_write(bufID, (android_LogPriority)priority, tag, msg);

    if (tag != NULL)
        env->ReleaseStringUTFChars(tagObj, tag);//这里调用了JNI函数释放资源
    env->ReleaseStringUTFChars(msgObj, msg);

    return res;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

从这里以看出,JNI层的实现方法只是根据一定的规则与Java层声明的方法做了一个映射,然后可以通过使用本地库函数或JNIEnv提供的JNI函数响应Java层调用。

3.Log 系统的JNI方法注册

JNI层已经实现了Java层声明的Native方法。可这两个方法又是如何联系在一起的呢?还是android_util_Log.cpp中的源码:

/*
 * JNI registration.
 */
static JNINativeMethod gMethods[] = {
    /* name, signature, funcPtr */
    { "isLoggable",      "(Ljava/lang/String;I)Z", (void*) android_util_Log_isLoggable },
    { "println_native",  "(IILjava/lang/String;Ljava/lang/String;)I", (void*) android_util_Log_println_native },
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这里定义了一个数组gMethods,用来存储JNINativeMethod类型的数据。 可以在jni.h文件中找到JNINativeMethod的定义:

typedef struct {
    const char* name;
    const char* signature;
    void*       fnPtr;
} JNINativeMethod;
  • 1
  • 2
  • 3
  • 4
  • 5

可见,JNINativeMethod是一个结构体类型,保存了声明函数和实现函数的一一对应关系。
看一下gMethods中的第一句:

( "isLoggable", "(Ljava/lang/String;I)2", (void*) android_util_Log_isLoggable } 
  • 1
  • Java 层声明的Native 函数名为isLoggable。
  • Java 层声明的Native函数的签名为(Ljava/lang/String;I)Z。
  • JNI层实现方法的指针为(void*)android_util_Log_isLoggable。

至此,我们给出了Java层方法和JNI层方法的对应关系。可如何告诉虚拟机这种对应关系呢?继续分析 android_util_Log.cpp源码。定位到以下部分:

int register_android_util_Log(JNIEnv* env)
{
    jclass clazz = FindClassOrDie(env, "android/util/Log");

    levels.verbose = env->GetStaticIntField(clazz, GetStaticFieldIDOrDie(env, clazz, "VERBOSE", "I"));
    levels.debug = env->GetStaticIntField(clazz, GetStaticFieldIDOrDie(env, clazz, "DEBUG", "I"));
    levels.info = env->GetStaticIntField(clazz, GetStaticFieldIDOrDie(env, clazz, "INFO", "I"));
    levels.warn = env->GetStaticIntField(clazz, GetStaticFieldIDOrDie(env, clazz, "WARN", "I"));
    levels.error = env->GetStaticIntField(clazz, GetStaticFieldIDOrDie(env, clazz, "ERROR", "I"));
    levels.assert = env->GetStaticIntField(clazz, GetStaticFieldIDOrDie(env, clazz, "ASSERT", "I"));

    return RegisterMethodsOrDie(env, "android/util/Log", gMethods, NELEM(gMethods));
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这个函数最后调用了 AndroidRuntime::registerNativeMethods,并将gMethods数组、Java 层类名以及一个JNIEnv类型的指针一同传给 registerNativeMethods。接着我们打开AndroidRuntime.cpp定位到registerNativeMethods函数:

/*
 * 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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

接着打开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);
    }

    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
        char* msg;
        asprintf(&msg, "RegisterNatives failed for '%s'; aborting...", className);
        e->FatalError(msg);
    }

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

这里最终调用了JNIEnv的RegisterNatives方法,将gMethods中存储的关联信息传递给虚拟机,在jni.h中找到RegisterNatives:

jint (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,jint);
  • 1

其作用是向jclass指定的类注册本地方法,这样细腻及就得到了java层与jni层的对应关系,就可以实现java与C/C++相互操作了。

到现在整个流程基本清晰了,最后来看看这个register_android_util_Log注册方法在哪里调用的呢?
原来,在Android系统启动的时候,由init进程启动Zygote的时候,会创建虚拟机,然后注册系统的jni方法,主要的代码:

void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
    ... ...
    
    if (startVm(&mJavaVM, &env, zygote) != 0) {
        return;
    }
    onVmCreated(env);

    /*
     * Register android functions.
     */
    if (startReg(env) < 0) {
        ALOGE("Unable to register all android natives\n");
        return;
    }

    ... ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

其中调用startReg:

/*static*/ int AndroidRuntime::startReg(JNIEnv* env)
{
    /*
     * This hook causes all future threads created in this process to be
     * attached to the JavaVM.  (This needs to go away in favor of JNI
     * Attach calls.)
     */
    androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);

    ALOGV("--- registering native functions ---\n");

    /*
     * Every "register" function calls one or more things that return
     * a local reference (e.g. FindClass).  Because we haven't really
     * started the VM yet, they're all getting stored in the base frame
     * and never released.  Use Push/Pop to manage the storage.
     */
    env->PushLocalFrame(200);

    if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
        env->PopLocalFrame(NULL);
        return -1;
    }
    env->PopLocalFrame(NULL);

    //createJavaThread("fubar", quickTest, (void*) "hello");

    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

再调用register_jni_procs,其中该函数有个gRegJNI参数,定位它定义的地方看一下:

static const RegJNIRec gRegJNI[] = {
    REG_JNI(register_com_android_internal_os_RuntimeInit),
    ... ...
    REG_JNI(register_android_util_Log),
    ... ...
  • 1
  • 2
  • 3
  • 4
  • 5

果然,其中包含的就是register_android_util_Log函数,这样,就在系统启动的时候就将Log系统的jni注册了
JNIEnv是一个指针,指向一个线程相关的结构,该结构维护了一个数组,该数组每个元素指向一个JNI函数,通过JNIEnv操作JNI函数,访问虚拟机,进而操作java对象

上面讲述的就是传说中的JNI动态注册方式,在Android的框架层主要就是以这种方式进行JNI的注册

三、在java中调用JNI实现方法

1.java数据类型与JNI数据类型的转换

  1. 基本类型转化关系
    在java中调用jni的native方法传递的参数是java类型的,这些参数必须经过Dalvik虚拟机转换成JNI类型的才能被JNI层所识别,如图:
    java类型JNI类型字长
    booleanjboolean8
    bytejbyte8
    charjchar16
    shortjshort16
    intjint32
    longjlong64
    floatjfloat32
    doublejdouble64
    voidvoid-

为了使用方便,特意定义:

# define JNI_FALSE 0
# define JNI_TRUE 1
typedef jint jsize;//jsize整数类型表示大小
  • 1
  • 2
  • 3
  1. 引用类型转换关系
    JNI的引用类型定义了九种数组类型,以及jobject、jclass、jstring、jthrowable四种类型,它们的继承关系如下图:
    JNI类型继承关系
    它们与java类型的对应关系如下:
    java类型JNI类型
    java.lang.Classjclass
    java.lang.Stringjstring
    java.lang.Throwablejthrowable
    Object[]、boolean[]、byte[]、char[]、short[]、int[]、long[]、float[]、double[]jobjectArray、jbooleanArray、jbyteArray、jcharArray、jshortArray、jintArray、jlongArray、jfloatArray、jdoubleArray
    java.lang.Objectjobject

2.JNI方法命名规则

Log系统中,JNI实现方法与Java声明方法是不同的。例如,Java层声明的Native方法名是isLoggable,而其对应的NI实现方法的方法名却是android_util_Log_isLoggable。可见,除了数据类型有对应关系外,方法名也有对应关系。

JNI接口指针是JNI实现方法的第一个参数,其类型是JNIEnv。第二个参数因本地方法是静态还是非静态而有所不同。非静态本地方法的第二个参数是对Java对象的引用,而静态本地方法的第二个参数是对其Java类的引用。其余的参数都对应于Java方法的参数。
JNI规范里提供了JNI实现方法的命名规则,方法名由以下几部分串接而成:

  • Java前缀
  • 全限定的类名
  • 下划线(_)分隔符
  • 增加第一参数JNIEnv* env
  • 增加第二个参数jobject
  • 其他参数按类型映射
  • 返回值按类型映射

继续以Log系统为例讲解。Java部分方法声明如下:

public static native boolean isLoggable(String tag,int level);
  • 1

JNI部分方法实现如下:

static jboolean android_util_Log_isLoggable(JNIEnv*env,jobject clazz,jstring tag,jint level).....)
  • 1

从Log系统的NI实现方法看到,Android并没有严格遵守JNI的方法命名规范。
Android在框架层采用函数注册的方式,建立Java层声明方法与JNI层实现方法之间的对应关系,可以不遵守上述命名规则。

严格按照这里的命名规则进行注册即静态注册,一般在进行NDK开发的时候采用这种方式比较方便

3.JNI方法签名规则

有了数据类型之间的对应关系,JNI就可以正确识别并转换Java类型。那JNI又是如何识别Java的方法呢?Java支持方法重载,仅靠函数名是无法唯一确定一个方法的。于是JNI提供了一套签名规则,用一个字符串来唯一确定一个方法。其规则如下:

(参数1类型签名参数2类型签名…参数n类型签名)返回值类型签名 //中间没有空格
  • 1

类型签名的规则如下表:

Java类型类型签名Java类型类型签名
booleanZlongJ
byteBfloatF
charCdoubleD
shortSL全限定类名;
intI数组[元素类型签名

还记得gMethods中的代码吧:

static JNINativeMethod gMethods[] = {
    /* name, signature, funcPtr */
    { "isLoggable",      "(Ljava/lang/String;I)Z", (void*) android_util_Log_isLoggable },
    { "println_native",  "(IILjava/lang/String;Ljava/lang/String;)I", (void*) android_util_Log_println_native },
};
  • 1
  • 2
  • 3
  • 4
  • 5

现在应该明白了isLoggable有两个参数,一个是String类型,一个是int类型,返回值是boolean。

四、JNI操作java对象

前面说到JNI是java与Native的桥梁,能相互操作,之前讲到的是java层调用Native方法,那Native层如何调用或操作java层呢?那么就需要关注JNI方法函数的第二个参数jobject

1.访问java对象

要操作jobject,就是要访问这个对象并操作它的变量和方法,JNI提供的类或对象操作函数有很多,常用的有两个:FindClass和GetObjectClass,在C和C++中分别有不同的函数原型。
C++中的函数原型如下:

jclass FindClass(const char* name); //查找类信息
jclass GetObjectClass(jobject obj); //返回对象的类
  • 1
  • 2

C中的函数原型如下:

jclass (*Findclass)(JNIEnv,const char);
jclass (*GetObjectClass)(JNIEnv*, jobject);
  • 1
  • 2

我们可以看看Log系统是怎么操作Java对象的。打开android_util_Log.cpp,定位到register_android_util_Log函数:

int register_android_util_Log(JNIEnv* env){
	jclass clazz = env->FindClass("android/util/Log");
	... ...
}
  • 1
  • 2
  • 3
  • 4

通过给FindClass传入要查找类的全限定类名(以“/”分隔路径)即可,之后方法返回一个jclass的对象,这样就可以操作这个类的方法和变量了。

2.操作成员变量和方法

上面我们拿到的类的信息clazz,通过这个就能操作它的变量和方法了

levels.debug = env->GetStaticIntField(clazz, env->GetStaticFieldID(clazz, "DEBUG", "I"));
  • 1

其中DEBUG是要访问的Java域的名字,I是该Java域的类型签名。GetStaticFieldID的函数原型如下:

jfieldID GetStaticFieldID(jclass clazz, const char* name, const char* sig)
  • 1

该函数返回了一个jfieldID,代表Java成员变量。最后将该jfieldID传给GetStaticIntField方法,得到Java层的成员变量DEBUG的值,即3。可以打开Log.java看看变量DEBUG的值是不是3。
下表中列出了JNI提供的操作域和方法的函数。

访问对象变量调用实例方法访问静态变量调用静态方法
GetFieldIDGetMethodIDGetStaticFieldIDGetStaticMethodID
Get<Type>FieldCall<Type>MethodGetStatic<Type>FieldCallStatic<Type>Method
Set<Type>FieldCallNonvirtual<Type>MethodSetStatic<Type>Field-

3.全局引用、弱全局引用和局部引用

Java对象的生命周期由虚拟机管理,虚拟机内部维护一个对象的引用计数,如果一个对象的引用计数为0,这个对象将被垃圾回收器回收并释放内存。这里就有一个问题,如果Java对象中使用了Native方法,那会对对象的生命周期产生什么影响呢?回答这个问题前,先看Log系统的例子。代码如下:

//static jobject clazz_ref1 = NULL;
static jboolean android_util_Log_isLoggable(JNIEnv* env, jobject clazz,jstring tag,jint level){
	//clazz_ref1 = clazz;
	//static jobject clazz_ref2 = NULL;
	//clazz_ref2 = clazz;
	if ((strlen(chars)+sizeof(LOG_NAMESPACE)) > PROPERTY _KEY_MAX)(
		//异常处理代码
		... ...
	}else{
		result = isLoggable(chars, level);
	}
	
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

上面注释掉的代码用两种方式引用java对象,这种形式的写法虚拟机能正确的为此对象添加引用计数吗???答案是不行,虚拟机不能正确的增加其计数,clazz对象就有可能被回收,那么clazz_ref1或clazz_ref2就可能引用野指针。

幸运的是,JNIEnv已经为我们提供了解决方案:局部引用、全局引用和弱全局引用。

  1. 局部引用:可以增加引用计数,作用范围为本线程,生命周期为一次Native调用。局部引用包括多数JNI函数创建的引用,Native方法返回值和参数。局部引用只在创建它的Native方法的线程中有效,并且只在Native方法的一次调用中有效,在该方法返回后,被虚拟机回收(不同于C中的局部变量,返回后会立即回收)。
  2. 全局引用:可以增加引用计数。作用范围为多线程,多个Native方法,生命周期到显式释放。全局引用通过JNI函数NewGlobalRef创建,并通过DeleteGlobalRef释放。如果程序员不显式释放,将永远不会被垃圾回收。
  3. 弱全局引用:不能增加引用计数。作用范围为多线程,多个Native方法,生命周期到显式释放。不过其对应的Java对象生命周期依然取决于虚拟机,意思是即便弱全局引用没有被释放,其引用的Java对象可能已经被释放。弱全局引用通过JNI函数NewWeakGlobalRef创建,并通过DeleteWeakGlobalRef释放。弱全局引用的优点是:既可以保存对象,又不会阻止该对象被回收。
    IsSameObject函数用来判断弱引用对应的对象是否已经被回收,方法是用弱全局引用和NULL进行比较,如果返回JNI_TRUE,则说明弱全局引用指向的对象已经被回收。
    所以上面的代码可以改成:
//static joject g_clazz_ref = NULL;
static jboolean android_util_Log_isLoggable(JNIEnv* env, jobject clazz,jstring tag,jint level){
	//g_class_ref = env->NewGlobalRef(clazz);
	if ((strlen(chars)+sizeof(LOG_NAMESPACE)) > PROPERTY _KEY_MAX)(
		//异常处理代码
		... ...
	}else{
		result = isLoggable(chars, level);
	}
}
//在不使用该类的时候显式删除
env->DeleteGlobalRef(g_clazz_ref)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

五、JNI异常处理

JNI提供两种检查异常的方法:

  1. 检查上一次JNI函数调用的返回值是否为NULL
  2. 通过调用JNI函数ExceptionOccurred()来判断是否发生异常

处理异常也有两种方式:

  1. Native方法可选择立即返回,异常会在调用噶Native方法的java代码中抛出,所以java层必须捕获
  2. Native方法调用ExceptionClear()来清除异常,然后执行自己的异常处理代码

JNI提供的检查和处理异常的函数如下表所示。

JNI异常处理函数功能描述
Throw抛出现有异常
ThrowNew抛出新的异常
ExceptionOccurred判断是否发生异常,并获得异常的引用
ExceptionCheck判断是否发生异常
ExceptionDescribe异常堆栈信息
ExceptionClear清除一个未处理的异常
FatalError严重错误,退出

注意异常出现后,Native相关代码必须先检查清除异常,然后才能进行其他的JNI函数调用。当有异常未被清除时,只有以下JNI异常处理函数可被安全地调用:ExceptionOccurred()、ExceptionDescribe()、ExceptionClear()
接下来我们回到源码分析一波:

static jboolean android_util_Log_isLoggable(JNIEnv* env, jobject clazz, jstring tag, jint level)
{
    if (tag == NULL) {
        return false;
    }

    const char* chars = env->GetStringUTFChars(tag, NULL);
    if (!chars) {
        return false;
    }

    jboolean result = false;
    if ((strlen(chars)+sizeof(LOG_NAMESPACE)) > PROPERTY_KEY_MAX) {
        char buf2[200];
        snprintf(buf2, sizeof(buf2), "Log tag \"%s\" exceeds limit of %zu characters\n",
                chars, PROPERTY_KEY_MAX - sizeof(LOG_NAMESPACE));

        jniThrowException(env, "java/lang/IllegalArgumentException", buf2);
    } else {
        result = isLoggable(chars, level);
    }

    env->ReleaseStringUTFChars(tag, chars);
    return result;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

找到其中的jniThrowException函数定义:

extern "C" int jniThrowException(C_JNIEnv* c_env, const char* className, const char* msg) {
    JNIEnv* env = reinterpret_cast<JNIEnv*>(c_env);
    jclass exceptionClass = env->FindClass(className);

    if (exceptionClass == NULL) {
        ALOGD("Unable to find exception class %s", className);
        /* ClassNotFoundException now pending */
        return -1;
    }

    if (env->ThrowNew(exceptionClass, msg) != JNI_OK) {
        ALOGD("Failed throwing '%s' '%s'", className, msg);
        /* an exception, most likely OOM, will now be pending */
        return -1;
    }

    env->DeleteLocalRef(exceptionClass);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

会看到调用ThrowNew向调用它的Java层抛出一个新的异常

六、两种注册方式的比较

1.静态注册

优点:符合JNI规范,方便简单
缺点:需要遵守繁琐的JNI实现方法的命名规则。加载共享库的代码,应用层若频繁调用严重影响效率。虚拟机在共享库中搜索定位JNI实现方法效率也受影响。

2.动态注册

优点:效率高,不需要遵守命名规则,修改、移植方便。
缺点:实现步骤比静态注册多,稍显麻烦。

如何在NDK开发中使用动态注册呢?上代码:

static JNINativeMethod gmethods[] = {
    {"show", "()Ljava/lang/String;, (void*)Java_com_hision_jni_AppJniActivity_show}
};

static int registerNativeMethods(JNIEnv* env, const char* className,JNINativeMethod* gMethods, int numMethods){
    jclass clazz;
    clazz = (*env)->FindClass(env, className);
    if (clazz == NULL) {
        return JNI_FALSE;
    }
    //调用JNIEnv提供的注册函数向虚拟机注册
    if ((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0){
        return JNI_FALSE;
    }
    return JNI_TRUE;

}

static int registerNatives(JNIEnv* env){
    if (!registerNativeMethods(env, "com/hision/jni/AppJniActivity", methods, sizeof(methods) / sizeof(methods[0]))) {
        return  JNI_FALSE;
    }
    return JNI_TRUE;
}

/*虚拟机执行System.loadLibrary("app_jni")后,进入libapp_jni.so后
*会首先执行这个方法,所以我们在这里做注册的动作*/

jint JNI_OnLoad (JavavM* vm, void* reserved){
    jint result = -1;
    JNIEnv* env = NULL;
    if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_4)){
        goto fail;
    }
    //最终调用(*env)->RegisterNatives,这跟Log系统是一样的
    if (registerNatives(env) != JNI_TRUE) {
        goto fail;
    }
    result = JNI_VERSION_1_4;

fail:
    return result;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

总结

本文以Log系统的JNI实例为引线,贯穿了JNI技术的主要方面,如果你真的认真读完本文你会对JNI有足够的认识,对于深人学习框架层代码会有一定的帮助。如果你在文章中发现有错误,欢迎指正!

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/575249
推荐阅读
相关标签
  

闽ICP备14008679号