什么是JNI
JNI是Java Native Interface的缩写
顾名思义,JNI是Java世界和Native世界的桥梁,这里的Native主要是指C和C++。
为什么要学习JNI
这个问题很好回答,因为Android系统源码中大量的使用了JNI技术.如果你只停留在Java层,那么永远不会真正理解Android, 虽然你到了Native层也不一定会理解。
怎么学习JNI
前面说了,Android Aosp中大量的使用了JNI, 那我们可以找个模块学习. 如果你是做Android Tv相关的,可以看TvInput模块和Tune Framework模块,这里选择后者。
loadLibrary
JNI的逻辑一般是动态库中实现的,Linux下一般是.so
文件,Windows下是.dll
文件,下面以Android Tv的Tuner Framework框架介绍一下加载动态库的过程,先找到Tuner这个java类,里面有个静态代码块,System.loadLibrary就是我们加载动态库的操作,Android中加载的是so库,参数就是我们要加载的so库的名称。
//frameworks/base/media/java/android/media/tv/tuner/Tuner.java
static {
try {
System.loadLibrary("media_tv_tuner");
nativeInit();
} catch (UnsatisfiedLinkError e) {
Log.d(TAG, "tuner JNI library not found!");
}
}
上面media_tv_tuner
加载的是下面的so库,so文件前面加了个lib.
/system/lib/libmedia_tv_tuner.so
这里,关于System.loadLibrary的过程,后面会单独出一篇文章介绍.
Java中定义Jni函数
在Java类中定义jni函数,只需要添加native关键字,而他的实现则是在上面加载的so库中(jni的实现可以是c也可以是c++,Aosp中主要用的是c++)。
//frameworks/base/media/java/android/media/tv/tuner/Tuner.java
/**
* Native setup.
*/
private native void nativeSetup();
Jni层中的实现
//frameworks/base/media/jni/android_media_tv_Tuner.cpp
static void android_media_tv_Tuner_native_setup(JNIEnv *env, jobject thiz) {
sp<JTuner> tuner = new JTuner(env, thiz);
setTuner(env,thiz, tuner);
}
上面就是java层nativeSetup()
方法的实现,咦~~~,有没有发现那里不对劲,定义的方法和实现的方法怎么不一样,而且还多了两个参数。一个在Java层一个在Native层怎么是怎么互相找到呢?别急,这里要讨论两种注册方式: 静态注册和动态注册。首先为啥要注册,注册就是让java层中定义的native方法和so中对应的方法绑定起来,生成一个函数映射,这样就能彼此找到了。
静态注册的方式
通过特殊的规则定义的Jni函数名称:以Java为前缀,并且用“_”下划线将包名、类名以及native方法名连接起来
Java_packagename_classname_methodname(JNIEnv *env,jclass/jobject,...)
静态注册的步骤:
- 在Java中定义native方法
- 用javah 和javac命令生成包含native方法的.h头文件
- 实现native方法
使用静态注册的方式,在运行的时候需要根据函数名称去找相关联的函数,效率比较低。而且函数的命名必须的符合上述的规则.
动态注册
静态注册方法太长了,可以使用动态注册的方式。通常我们在JNI_OnLoad
方法中完成动态注册。
JNI_OnLoad
调用System.loadLibrary()函数时, 内部就会去查找so中的 JNI_OnLoad 函数,如果存在此函数,则调用。 JNI_OnLoad 必须返回 JNI 的版本,比如 JNI_VERSION_1_6、JNI_VERSION_1_8。
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");
return result;
}
assert(env != NULL);
if (!register_android_media_tv_Tuner(env)) {
ALOGE("ERROR: Tuner native registration failed\n");
return result;
}
return JNI_VERSION_1_4;
}
static bool register_android_media_tv_Tuner(JNIEnv *env) {
if (AndroidRuntime::registerNativeMethods(
env, "android/media/tv/tuner/Tuner", gTunerMethods, NELEM(gTunerMethods)) != JNI_OK) {
ALOGE("Failed to register tuner native methods");
return false;
}
···
}
static const JNINativeMethod gTunerMethods[] = {
{ "nativeInit", "()V", (void *)android_media_tv_Tuner_native_init },
{ "nativeSetup", "()V", (void *)android_media_tv_Tuner_native_setup },
{ "nativeGetFrontendIds", "()Ljava/util/List;"
...
}
通过AndroidRuntime::registerNativeMethods
将Java层中的native方法和Jni层的c++方法映射起来,传入的参数就是一个JNINativeMethod
数组, 上面的JNI_OnLoad方法我们需要稍微详细的讲一下,这个方法给我们带来了两个概念JavaVM和JNIEnv.
- JavaVM
这个代表java的虚拟机。所有的工作都是从获取虚拟机的接口开始的。在Android一个进程对应一个JavaVM.
- JNIEnv
是提供JNI Native函数的基础环境,线程相关,不同线程的JNIEnv相互独立。JNIEnv只在当前线程中生效,本地方法不能将JNIEnv从一个线程传递到另外一个线程,所以不要缓存JNIEnv*.通过JNIEnv可以方便的调用Java函数和操作jobject对象,本文后面会介绍一些JNIEnv操作Java函数的一些用法。
数据类型
前面的注册解决了方法的映射,那么还有个问题就是数据类型的映射。
- 基本数据类型的映射
Java数据类型 | Jni数据类型 | C++类型 |
---|---|---|
boolean | jboolean | bool |
int | jint | int |
long | jlong | long |
byte | jbyte | bype |
char | jchar | char |
short | jshort | short |
float | jfloat | float |
double | jdouble | double |
-
引用类型
Java引用类型 Jni类型 Object jobject Class jclass String jstring Object[] jobjectArray bype[] jbypeArray Throwable jthrowable
如果想看完整的Jni类型,可以在
libnativehelper/include_jni/jni.h
中查看.
/* Primitive types that match up with Java equivalents. */
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
/* "cardinal indices and sizes" */
typedef jint jsize;
#ifdef __cplusplus
/*
* Reference types, in C++
*/
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};
class _jbyteArray : public _jarray {};
class _jcharArray : public _jarray {};
class _jshortArray : public _jarray {};
class _jintArray : public _jarray {};
class _jlongArray : public _jarray {};
class _jfloatArray : public _jarray {};
class _jdoubleArray : public _jarray {};
class _jthrowable : public _jobject {};
typedef _jobject* jobject;
typedef _jclass* jclass;
typedef _jstring* jstring;
typedef _jarray* jarray;
typedef _jobjectArray* jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray* jbyteArray;
typedef _jcharArray* jcharArray;
typedef _jshortArray* jshortArray;
typedef _jintArray* jintArray;
typedef _jlongArray* jlongArray;
typedef _jfloatArray* jfloatArray;
typedef _jdoubleArray* jdoubleArray;
typedef _jthrowable* jthrowable;
typedef _jobject* jweak;
Java层调用Jni层
在Java层已经定义了native方法,并且和native方法做了映射,这个使用比较简单, 直接调用java的方法就可以实现Java层调用Jni层了。
Jni层怎么返回一个Java对象
一个Java对象本质是由成员变量和成员函数构成的,操作一个Java对象就是操作其成员函数和成员变量.比如下面的代码,
我们在Jni层创建一个String对象。
/*
* convert c++ string to jstring
*/
static jstring stringTojstring(JNIEnv *env, const char *pat)
{
jclass strClass = env->FindClass("java/lang/String");
jmethodID ctorID = env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");
jbyteArray bytes = env->NewByteArray(strlen(pat));
(env)->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte *)pat);
jstring encoding = env->NewStringUTF("UTF-8");
return (jstring)env->NewObject(strClass, ctorID, bytes, encoding);
}
-
FindClass
获取jclass对象,用于获取函数的ID和创建实例,这个过程和我们Java使用反射的流程是一样的。
-
GetMethodID
获取MethodID, 用于后面的调用,这里的""代表构造函数,后面构造函数的签名,[B代表第一个参数是byte数组,Ljava/lang/String; 代表第二个参数是一个String类型,V代表返回值是Void,后面会给一张表介绍函数签名。
-
NewByteArray
生成一个byte数组对象
-
SetByteArrayRegion
给byte数组赋值。
-
NewObject
生成构造方法的形式生成我们的String对象
整个流程其实和反射非常的相似。
Jni层调用一个Java方法
下面的例子演示一下怎么在Jni中调用一下ArrayList的add方法,添加一个Integer对象。
jclass arrayListClazz = env->FindClass("java/util/ArrayList");
jmethodID arrayListAdd = env->GetMethodID(arrayListClazz, "add", "(Ljava/lang/Object;)Z");
jobject obj = env->NewObject(arrayListClazz, env->GetMethodID(arrayListClazz, "<init>", "()V"));
jclass integerClazz = env->FindClass("java/lang/Integer");
jmethodID intInit = env->GetMethodID(integerClazz, "<init>", "(I)V");
for (int i=0; i < 10; i++) {
jobject idObj = env->NewObject(integerClazz, intInit, i);
env->CallBooleanMethod(obj, arrayListAdd, idObj);
}
-
GetMethodID
获取add方法的method id.
-
CallBooleanMethod
obj代表ArrayList对象,arrayListAdd是add的method id, idObj是add的参数,这里是一个Integer对象。
Native线程回调Java层
前面说了JNIEnv是线程相关的,如果我们想在一个native的线程里回调到Java层,因为回调Java层需要用到JNIEnv,而这个时候我们又不能用别的线程里的JNIEnv,这该怎么办呢?
通过下面的方式:
JNIEnv *env = nullptr;
JavaVM->AttachCurrentThread(&env, NULL);
我们知道JavaVM是进程中唯一的,通过JavaVM
的AttachCurrentThread
方法,就可以得到这个线程的JNIEnv。通过JNIEnv就可以回到Java层了。
完事以后记得
JavaVM->DetachCurrentThread();
类型签名
前面在动态注册方法的时候,其实我们已经使用了方法签名,
static const JNINativeMethod gTunerMethods[] = {
{ "nativeInit", "()V", (void *)android_media_tv_Tuner_native_init },
{ "nativeSetup", "()V", (void *)android_media_tv_Tuner_native_setup },
{ "nativeGetFrontendIds", "()Ljava/util/List;"
...
}
比如"()V"
代表参数为空,返回值为V.,参数在括号里,返回值在括号外,参数需要按顺序排放,如果参数是引用类型则以L
开头, 跟全类名再加分号(;),例如:Ljava/util/List;
下面是类型签名表:
签名 | 类型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L全类名; | 类名 |
[签名类型 例如:[I 代表int[] | 数组 |
V | void |
签名很容易写错,java提供了一个javap的工具,可以生成签名信息,用法如下:
javap -s youclass.class
总结
Jni在系统开发中,一般给app封装接口时用到,但是,更重要的,作为Java世界和Native世界的纽带,它是我们学习Android系统源码的必备技能。
关于我
- 公众号: CodingDev