Android框架层基础2————JNI原理
Android框架层基础2————JNI原理
源码基于Android8.0分析
文章目录
- Android框架层基础2————JNI原理
- 一.JNI概述
- 二.基于静态注册的JNI使用
- 1.java中生明native方法
- 2.获得jni的头文件
- 3.实现JNI方法
- 4.小结
- 三.基于动态注册的android源码
- 1.源码中jni使用
- 2.动态注册的实现
- 四.数据类型的转化
- 1.基本类型的转换
- 2.引用类型的转换
- 3.方法签名
- 五.JNIEnv介绍
- 1.JNIEnv概述
- 2. JNIEnv的定义
- 3.jfieldID和jmethodID
- 六.JNI和垃圾回收
- 1.本地引用
- 2.全局引用
- 3.弱全局引用
- 七.JNI中的异常处理
- 八.参考资料
一.JNI概述
JNI 即java Native Interface缩写,即java本地调用。通过jni可以做到以下两点:
- java程序中的函数可以调用Native语言书写的函数,Native一般指的c/c++编写的函数
- Native程序的函数可以调用java中的函数
在Android源码中,jni大量的使用,同时也有很多的应用的场景,比如音视频开发,热修复,插件化,逆向开发,源码调用。接下来先看看jni的使用
二.基于静态注册的JNI使用
关于Jni的使用详细的步骤可以参考这篇博客
Android JNI学习(二)——实战JNI之“hello world”
下面我简单说一下jni的调用
具体的环境配置相关的,可以参考上面博客。
1.java中生明native方法
public class JniTest {static {System.loadLibrary("jni-test");}public static void main(String[] args) {JniTest jniTest = new JniTest();System.out.printf(jniTest.get());}public static native String get();public static native void set(String str);
}
上面的类中,先在静态代码块中,加载了动态库的过程,同时声明了两个native方法,get和set。
2.获得jni的头文件
- 根据javac生成class文件
- 在根据javah生成头文件
- javac 包名/JniTest.java(包名以/分割)
- javah 包名.JniTest(包名以.分割)
生成的一个com_heshucheng_androidjni_JniTest头文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_heshucheng_androidjni_JniTest */#ifndef _Included_com_heshucheng_androidjni_JniTest
#define _Included_com_heshucheng_androidjni_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/** Class: com_heshucheng_androidjni_JniTest* Method: get* Signature: ()Ljava/lang/String;*/
JNIEXPORT jstring JNICALL Java_com_heshucheng_androidjni_JniTest_get(JNIEnv *, jclass);/** Class: com_heshucheng_androidjni_JniTest* Method: set* Signature: (Ljava/lang/String;)V*/
JNIEXPORT void JNICALL Java_com_heshucheng_androidjni_JniTest_set(JNIEnv *, jclass, jstring);#ifdef __cplusplus
}
#endif
#endif
说明
- 函数名遵循下面规则:Java_包名_类名_方法
- jstring是代表String类型的参数
- JNIEnv:表示一个指向JNI环境的指针,可以通过它来访问JNI提供的接口方法
- jobject:表示Java对象中的this
- JNIEXPORT 和 JNICALL:它两是JNI中所定义的宏,可以在jni.h中查到定义
3.实现JNI方法
接下来就是实现jni方法,以C实现为例.
创建一个子目录,名称随意。将之前生成的头文件复制到该目录下。并创建test.c文件
#include "com_heshucheng_androidjni_JniText"
#include <stdio.h>
JNIEXPORT jstring JNICALL Java_com_heshucheng_androidjni_JniTest_get(JNIEnv *env, jobject obj){printf("invoke get");return (*env)->NewStringUTF(env,"Hellow form JNI");}JNIEXPORT void JNICALL Java_com_heshucheng_androidjni_JniTest_set(JNIEnv *env, jclass obj, jstring,string){printf("invoke set");char* str = (char *)(* env)->GetStringUTFChars(env,string,NULL);printf("%s\n"str);(* env)->ReleaseStringUTFChars(env,string,str) }
最后编译so库,并在java中调用。
4.小结
上面的这种方式我们称为静态注册。
这种情景下,当我们在java中调用native方法get时,就会从JNI中寻找Java_com_heshucheng_androidjni_JniTest_get函数,如果没有就报错,如果找到就和其建立联系,其实就是报存JNI函数指针,这样再次调用native_init方法时直接使用这个函数指针就可以了。
静态注册就是个人那就方法名,将java的native方法通过方法指针和JNI进行关联。如果java的Native方法知道它在JNI中的函数指针,就可以避免上述缺点。
三.基于动态注册的android源码
下面我就以AudioRecord源码中的jni调用为例,分析jni
1.源码中jni使用
下面看看AudioRecord.java中的stop方法
public void stop()throws IllegalStateException {if (mState != STATE_INITIALIZED) {throw new IllegalStateException("stop() called on an uninitialized AudioRecord.");}// stop recordingsynchronized(mRecordingStateLock) {handleFullVolumeRec(false);native_stop();mRecordingState = RECORDSTATE_STOPPED;}}......private native final void native_stop();
这个native方法的实习是在android_media_AudioRecord中的android_media_AudioRecord_stop
目录:framework/base/core/jni/android_media_AudioRecord.cpp
android_media_AudioRecord_stop(JNIEnv *env, jobject thiz)
{sp<AudioRecord> lpRecorder = getAudioRecord(env, thiz);if (lpRecorder == NULL ) {jniThrowException(env, "java/lang/IllegalStateException", NULL);return;}lpRecorder->stop();//ALOGV("Called lpRecorder->stop()");
}
2.动态注册的实现
在JNI中有一个种结构体用来记录java的Native方法和JNI方法的关联关系。它就是JNINativeMethod,它在jni.h中被定义
typedf struct{coust char* name;//java方法的名字、 coust char* signture;//java方法的签名信息void* fnPtr;//JNI中对应的方法指针
}
android_media_AudioRecord中有一个数组gMethods
目录:framework/base/core/jni/android_media_AudioRecord.cpp
static const JNINativeMethod gMethods[] = {// name, signature, funcPtr{"native_start", "(II)I", (void *)android_media_AudioRecord_start},{"native_stop", "()V", (void *)android_media_AudioRecord_stop},{"native_setup", "(Ljava/lang/Object;Ljava/lang/Object;[IIIII[ILjava/lang/String;J)I",(void *)android_media_AudioRecord_setup},{"native_finalize", "()V", (void *)android_media_AudioRecord_finalize},{"native_release", "()V", (void *)android_media_AudioRecord_release},{"native_read_in_byte_array","([BIIZ)I",(void *)android_media_AudioRecord_readInArray<jbyteArray>},{"native_read_in_short_array","([SIIZ)I",(void *)android_media_AudioRecord_readInArray<jshortArray>},{"native_read_in_float_array","([FIIZ)I",(void *)android_media_AudioRecord_readInArray<jfloatArray>},{"native_read_in_direct_buffer","(Ljava/lang/Object;IZ)I",(void *)android_media_AudioRecord_readInDirectBuffer},{"native_get_buffer_size_in_frames","()I", (void *)android_media_AudioRecord_get_buffer_size_in_frames},{"native_set_marker_pos","(I)I", (void *)android_media_AudioRecord_set_marker_pos},{"native_get_marker_pos","()I", (void *)android_media_AudioRecord_get_marker_pos},{"native_set_pos_update_period","(I)I", (void *)android_media_AudioRecord_set_pos_update_period},{"native_get_pos_update_period","()I", (void *)android_media_AudioRecord_get_pos_update_period},{"native_get_min_buff_size","(III)I", (void *)android_media_AudioRecord_get_min_buff_size},{"native_setInputDevice", "(I)Z", (void *)android_media_AudioRecord_setInputDevice},{"native_getRoutedDeviceId", "()I", (void *)android_media_AudioRecord_getRoutedDeviceId},{"native_enableDeviceCallback", "()V", (void *)android_media_AudioRecord_enableDeviceCallback},{"native_disableDeviceCallback", "()V",(void *)android_media_AudioRecord_disableDeviceCallback},{"native_get_timestamp", "(Landroid/media/AudioTimestamp;I)I",(void *)android_media_AudioRecord_get_timestamp},
};
gMethods数组存储的是AudioRecord的Native方法与JNI层函数的对应关系,但是只有这个gMethods数组还是不够的,还需要进行注册。
注册的函数是:register_android_media_AudioRecord
目录:framework/base/core/jni/android_media_AudioRecord.cpp
int register_android_media_AudioRecord(JNIEnv *env)
{.....return RegisterMethodsOrDie(env, kClassPathName, gMethods, NELEM(gMethods));
}
继续追踪
目录:framework/base/core/jni/core_jni_helpers.h
static inline int RegisterMethodsOrDie(JNIEnv* env, const char* className,const JNINativeMethod* gMethods, int numMethods) {int res = AndroidRuntime::registerNativeMethods(env, className, gMethods, numMethods);LOG_ALWAYS_FATAL_IF(res < 0, "Unable to register native methods.");return res;
}
在这其中返回了AndroidRuntime的registerNativeMethods函数,继续追踪
目录:framework/base/core/jni/AndroidRuntime
const char* className, const JNINativeMethod* gMethods, int numMethods)
{return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
在registerNativeMethods函数中又返回了jniRegisterNativeMethods,他被定义再JNI类帮助类的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* tmp;const char* msg;if (asprintf(&tmp,"Native registration unable to find class '%s'; aborting...",className) == -1) {// Allocation failed, print default warning.msg = "Native registration unable to find class; aborting...";} else {msg = tmp;}e->FatalError(msg);}if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {char* tmp;const char* msg;if (asprintf(&tmp, "RegisterNatives failed for '%s'; aborting...", className) == -1) {// Allocation failed, print default warning.msg = "RegisterNatives failed; aborting...";} else {msg = tmp;}e->FatalError(msg);}return 0;
}
在这里我们可以看见,最终调用了JNIENV的RegisterNatives来完成JNI的注册。关于JNIENV在后面会有比较详细的说明。
四.数据类型的转化
上面我们解决了jni的注册问题,接下来让我们看看jni的数据转换问题。在java中调用Native函数传递的是java类型参数,这些参数到jni层会转为不同的参数。
java的数据类型分为基本数据类型和引用数据类型。jni对于两者是区别对待的。
1.基本类型的转换
基本类型转换比较简单。具体如下表所示,最后一列代表签名格式,后面会介绍它。
| Java | Native类型 | 符号属性 | 字长 | 签名格式 |
|---|---|---|---|---|
| boolean | jboolean | 无符号 | 8位 | B |
| byte | jbyte | 无符号 | 8位 | C |
| char | jchar | 无符号 | 16位 | D |
| short | jshort | 有符号 | 16位 | F |
| int | jint | 有符号 | 32位 | I |
| long | jlong | 有符号 | 64位 | S |
| float | jfloat | 有符号 | 32位 | J |
| double | jdouble | 有符号 | 64位 | Z |
| void | void | V |
上面的基本类型,除了最后一行,其他只要在前面加上j即可
2.引用类型的转换
| java引用类型 | native | 签名格式 |
|---|---|---|
| All objects | jobject | L+classname +; |
| Class | jclass | Ljava/lang/Class; |
| String | jstring | Ljava/lang/String; |
| Throwable | jthrowable | Ljava/lang/Throwable; |
| Object[] | jobjectArray | [L+classname +; |
| boolean[] | jbooleanArray | [Z |
| byte[] | jbyteArray | [B |
| char[] | jcharArray | [C |
| short[] | jshortArray | [S |
| int[] | jintArray | [I |
| long[] | jlongArray | [J |
| float[] | floatArray | [F |
| double[] | jdoubleArray | [D |
从上图可以看出,所有的数组的JNI层数据类型需要以“Array”结尾,签名格式的开头都有“[”
引用数据类型也是具有继承关系的,如下图所示
我们以之前静态注册生成的头文件类型分析
Java_com_heshucheng_androidjni_JniTest_set(JNIEnv *env, jclass obj, jstring,string)
可以看出,java层的string 在jni层变成了就string类型
3.方法签名
前面我们看每个了类型后面都有一个签名格式,方法签名就是由签名格式组成的,那么,方法签名有什么作用呢?我们回到前面的gMethods数组中
static const JNINativeMethod gMethods[] = {
...{"native_release", "()V", (void *)android_media_AudioRecord_release},{"native_read_in_byte_array", "([BIIZ)I",(void *)android_media_AudioRecord_readInArray<jbyteArray>},...}
在gMethods数组中的, “()V"和”([BIIZ)I"就是签名方法。
产生原因
我们都知道java是由重载方法的,可以定义方法名相同,反参数不同的方法,正因为如此,jni中仅仅通过方法名时无法找到java中具体方法的,JNI为了解决这一问题就将参数类型和返回值组合在一起作为方法签名。通过方法签名和方法名就可以一起找到对应的java方法。、、
签名格式
(参数签名格式…)返回值签名格式
在生成方法签名的时候,java也提供了javap命令来自动生成方法签名,具体使用如下
javap -s - p 路径/类目.class
其中s表示输出内部类型签名,p表示打印所有方法和成员(默认打印public),最终会在cmd中输出结果。
五.JNIEnv介绍
1.JNIEnv概述
JNIEnv是一个指向全部JNI方法的指针。该指针只在创建它的线程有效,不能跨进程传递,因此不同线程的JNIEnv是彼此独立的,JNIEnv的主要作用有两点:
- 调用java方法
- 操作java(获取java中的变量和对象)
JNIEnv内部结构
[外链图片转存失败(img-BytDM08d-1566559589146)(http://wiki.jikexueyuan.com/project/deep-android-v1/images/chapter2/image003.png)]
2. JNIEnv的定义
下面我们来看看JNIEnv的定义:
目录:libnativehelper/include_Jni/nativehelper/jni.h
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv; //c++中JNIEnv定义
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;//c中的JNI类型
typedef const struct JNIInvokeInterface* JavaVM;
#endif
上面的代码中,使用__cplusplus来区分c和c++两种代码,可以看到c++类型是_JNIEnv,c是JNINativeInterface。继续用查看定义
目录:libnativehelper/include_Jni/nativehelper/jni.h
struct _JNIEnv {/* do not rename this; it does not seem to be entirely opaque */const struct JNINativeInterface* functions;#if defined(__cplusplus)...//寻找java指定名称的类jclass FindClass(const char* name) { return functions->FindClass(this, name); }//得到java中的方法jmethodID GetMethodID(jclass clazz, const char* name, const char* sig){ return functions->GetMethodID(this, clazz, name, sig); }//得到java中的成员变量jfieldID GetFieldID(jclass clazz, const char* name, const char* sig){ return functions->GetFieldID(this, clazz, name, sig); }
...
}
_JNIEnv是一个结构体,它的内部又包含了JNINativeInterface,在_JNIEnv中定义了很多函数,上面列举了三个常见的函数。同时也可以发现,无论,最终他们还是调用了JNINativeInterface中定义的函数。
来看看JNINativeInterface中的定义
struct JNINativeInterface {
....jclass (*FindClass)(JNIEnv*, const char*);jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);jfieldID (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
···
}
在JNINativeInterface结构中定义了很多和JNIEnv结构体对应的函数指针,上面只是给了3个函数对应的函数指针定义。通过这些函数指针的定义,就能够定位到虚拟机中的JNI函数表,从而实现了JNI层可以调用Java世界的方法了。
如何在同一个进程,但是不同线程中调用java方法
在上面的源码中,我们可以发现一个JavaVM,他是虚拟机在JNI层的代表,在一个虚拟机进程中只存在一个JavaVM,因此,该进程所以的线程都可以使用这个JavaVM,通过调用Java的AttchCurrent函数可以获取这个线程的JNIEnv,这样就可以在不同的线程中调用java方法了,还要记得在使用AttachCurrentThread函数的线程退出前,务必要调用DetachCurrentThread函数来释放资源
3.jfieldID和jmethodID
前面我们知道,通过JNIEnv可以操作java对象和方法。那么他是如果实现的?
我们知道,其实一个java对象实际上是由它的成员对象和成员函数来操作的。所以在JNI规则中,用jfieldID 和jmethodID 来表示Java类的成员变量和成员函数,它们通过JNIEnv的下面两个函数可以得到:
jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);
其中,jclass代表Java类,name表示成员函数或成员变量的名字,sig为这个函数和变量的签名信息。如前所示,成员函数和成员变量都是类的信息,这两个函数的第一个参数都是jclass。
获得jfieldID和jmethodID的过程
jclass clazz;//Java层的MediaRecorder的Class对象clazz = env->FindClass("android/media/MediaRecorder");if (clazz == NULL) {return;}//获取jfieldID对象fields.context = env->GetFieldID(clazz, "mNativeContext", "J");//2if (fields.context == NULL) {return;}//获取jmethodID对象fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative","(Ljava/lang/Object;IIILjava/lang/Object;)V");//4if (fields.post_event == NULL) {return;}
当获取成功之后就可以直接调用到java
//传入了上面获得的jmethodID对象
env->CallStaticVoidMethod(mClass, fields.post_event, mObject, msg, ext1, ext2, NULL);//1
六.JNI和垃圾回收
在java中,存在四种引用类型,强软弱虚,这四种类型对虚拟机回收垃圾有不同程度的影响。同样的,在JNI中,也存在不同的引用类型,即本地引用,全局引用,弱全局引用,下面分别来介绍他们
1.本地引用
JNIEnv提供的函数所提供的引用类型基本都是本地引用,因此本地引用也是JNI中最常见的引用类型,本地引用的特点如下:
- 当Native函数返回时,这个本地引用就会自动被释放
- 只在创建它的线程有效,不能跨线程使用
- 局部引用是JVM负责的引用类型,受JVM管控
2.全局引用
全局引用和本地引用几乎是相反的,它主要有以下特点:
- 在Native函数返回时不会被自动释放掉,因此全局引用需要手动来进行释放,并且不会被GC掉
- 全局引用是可以跨线程使用的
- 全局引用不受到JVM的管控
全局引用是通过JNIEnv的NewGlobalRef函数用来创建全局引用,调用JNIEnv的DeleteGlobalRef函数来释放全局引用
3.弱全局引用
弱全局引用是一种特殊的全局引用,它和全局引用的特点相似,不同的是弱全局引用是可以被gc回收的,弱全局引用被GC回收后,弱全局引用被GC回收之后会指向NULL,所以在访问弱全局引用之前,要首先判断它是否被回收了,方法就是JNIEnv的isSameObject函数来判断
弱全局引用是通过NewWeakGlobalRef函数创建的,DeleteWeakGlobalRef来进行释放
七.JNI中的异常处理
NI中也有异常,不过它和C++、Java的异常不太一样。当调用JNIEnv的某些函数出错后,会产生一个异常,但这个异常不会中断本地函数的执行,直到从JNI层返回到Java层后,虚拟机才会抛出这个异常。虽然在JNI层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能做一些资源清理工作了(例如释放全局引用,或者ReleaseStringChars)。如果这时调用除上面所说函数之外的其他JNIEnv函数,则会导致程序死掉。
virtualbool scanFile(const char* path, long long lastModified,long long fileSize){jstring pathStr;//NewStringUTF调用失败后,直接返回,不能再干别的事情了。if((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;......}
JNI层函数可以在代码中截获和修改这些异常,JNIEnv提供了三个函数进行帮助:
- ExceptionOccured函数,用来判断是否发生异常。
- ExceptionClear函数,用来清理当前JNI层中发生的异常。
- ThrowNew函数,用来向Java层抛出异常。
异常处理是JNI层代码必须关注的事情,读者在编写代码时务小心对待。
八.参考资料
《Android艺术开发探索》
《深入理解Android 卷一》
《Android进阶解密》
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
