以下内容主要参考深入理解Android卷1.
JNI函数动态注册:
使用这个数据结构存储一个JNI函数的注册信息:
typedef struct {
//Java中native函数的名字,不用携带包的路径。例如“native_init“。
constchar* name;
//Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。
const char* signature;
void* fnPtr; //JNI层对应函数的函数指针,注意它是void*类型。
} JNINativeMethod;
从以上成员看出java中声明的native函数好像可以对应上该jni文件中的函数,但是实际上jni函数的在这个结构上是唯一的,但是java声明的不是,因为不带包路径,只是一个函数名,怎么知道这个函数是哪个类的呢,如果其他java文件也有这样一个同名的native声明呢?其实在注册时,会去获取对应的java类对象(Class类型的对象)。
为JNI动态库中的所有JNI函数建立一个JNINativeMethod数组,注册就用这个数组作为参数。示例:
static JNINativeMethod g_methods[] = {
{"nativeSetBlueLightFilterStrength", "(I)Z", (void*)nativeSetBlueLightFilterStrength},
{"nativeGetBlueLightFilterStrength", "()I", (void*)nativeGetBlueLightFilterStrength},
{"nativeEnableBlueLightFilter", "(Z)Z", (void*)nativeEnableBlueLightFilter},
{"nativeIsBlueLightFilterEnabled", "()Z", (void*)nativeIsBlueLightFilterEnabled},
{"nativeGetBlueLightFilterStrengthRange", "()I", (void*)nativeGetBlueLightFilterStrengthRange},
{"nativeBlueLightFilterInit", "()Z", (void*)nativeBlueLightFilterInit},
};
JNI函数签名:
JNI规范定义的函数签名信息格式:
(参数1类型标示参数2类型标示...参数n类型标示)返回值类型标示
比较繁琐,具体编码时,读者可以定义字符串宏
类型标示示意表
类型标示 |
Java类型 |
类型标示 |
Java类型 |
Z |
boolean |
F |
float |
B |
byte |
D |
double |
C |
char |
Ljava/lang/String; |
String |
S |
short |
[I |
int[] |
I |
int |
[Lava/lang/object; |
Object[] |
J |
long |
|
|
上面列出了一些常用的类型标示,如果Java类型是数组,则标示中会有一个“[”,另外,引用类型(除基本类型的数组外)的标示最后都有一个“;”,返回值签名时也是要加分号的
函数签名小例子
函数签名 |
Java函数 |
“()Ljava/lang/String;” |
String f() |
“(ILjava/lang/Class;)J” |
long f(int i, Class c) |
“([B)V” |
void f(byte[] bytes) |
当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数,如果有,就调用它,而动态注册的工作就是在这里完成的。
在JNI_OnLoad函数中注册会用到一下两行代码:
/*
env指向一个JNIEnv结构体,classname为对应的Java类名(全路径名,包名+类名),由于
JNINativeMethod中使用的函数名并非全路径名,所以要指明是哪个类。
*/
jclass clazz = (*env)->FindClass(env, className);
//调用JNIEnv的RegisterNatives函数,注册关联关系。
(*env)->RegisterNatives(env, clazz, gMethods,numMethods);
示例:
/*
* Register several native methods for one class.
*/
static int registerNativeMethods(JNIEnv* env, const char* className,
JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
clazz = env->FindClass(className);
if (clazz == NULL) {
ALOGE("Native registration unable to find class '%s'", className);
return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
ALOGE("RegisterNatives failed for '%s'", className);
return JNI_FALSE;
}
return JNI_TRUE;
}
// ----------------------------------------------------------------------------
/*
* This is called by the VM when the shared library is first loaded.
*/
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
//该函数的第一个参数类型为JavaVM,这可是虚拟机在JNI层的代表喔,每个Java进程只有一个
JNIEnv* env = NULL;
jint result = -1;
UNUSED(reserved);
ALOGI("JNI_OnLoad");
if (JNI_OK != vm->GetEnv((void **)&env, JNI_VERSION_1_4)) {
ALOGE("ERROR: GetEnv failed");
goto bail;
}
if (!registerNativeMethods(env, classPathName, g_methods, sizeof(g_methods) / sizeof(g_methods[0]))) {
ALOGE("ERROR: registerNatives failed");
goto bail;
}
//必须返回这个值,否则会报错。
result = JNI_VERSION_1_4;
bail:
return result;
}
静态注册:
extern "C" JNIEXPORT jstring
JNICALL
Java_com_champion_ndkhelloword_MainActivity_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
必须带上extern "C",但是JNIEXPORT和JNICALL是可以带也可以不带上。不影响程序执行,只是标记作用。动态注册则不用带上extern "C"。开始猜想可能是因为动态注册时带上了签名,包含了参数类型,这就符合了C++对重载函数的描述要求,而静态注册则只会根据名字查找。但是后面实践证明,无论是动态注册还是静态注册都不允许JNI函数重载。
在一个Java文件中,允许部分native函数是静态注册,而部分是动态注册的。
-----------------------------------------------------------------------------------------------
JNI层代码中一般要包含jni.h这个头文件。Android源码中提供了一个帮助头文件JNIHelp.h,它内部其实就包含了jni.h,所以我们在自己的代码中直接包含这个JNIHelp.h即可。
头文件路径可以通过在Android.mk中指定:
LOCAL_C_INCLUDES := $(JNI_H_INCLUDE) \
$(MTK_PATH_SOURCE)/hardware/pq/mt8163/inc
java调用native函数时,可以传递任意java类型的对象。在jni函数中,并不能识别具体的对象类型,jni函数通过类似java反射的技术调用java层传过来的对象的成员和方法。
2. 数据类型转换
通过前面的分析,解决了JNI函数的注册问题。下面来研究数据类型转换的问题。
在Java中调用native函数传递的参数是Java数据类型,那么这些参数类型到了JNI层会变成什么呢?
Java数据类型分为基本数据类型和引用数据类型两种,JNI层也是区别对待这二者的。先来看基本数据类型的转换。
(1)基本类型的转换
基本类型的转换很简单,可用表2-1表示:
表2-1 基本数据类型转换关系表
Java |
Native类型 |
符号属性 |
字长 |
boolean |
jboolean |
无符号 |
8位 |
byte |
jbyte |
无符号 |
8位 |
char |
jchar |
无符号 |
16位 |
short |
jshort |
有符号 |
16位 |
int |
jint |
有符号 |
32位 |
long |
jlong |
有符号 |
64位 |
float |
jfloat |
有符号 |
32位 |
double |
jdouble |
有符号 |
64位 |
上面列出了Java基本数据类型和JNI层数据类型对应的转换关系,非常简单。不过,应务必注意,转换成Native类型后对应数据类型的字长,例如jchar在Native语言中是16位,占两个字节,这和普通的char占一个字节的情况完全不一样。上面的基本类型除了注意类型所占字节长度外,在jni中可以混合使用,不会报错。如一个JNIEnv中的方法要求jint类型的,可以直接传一个int类型的,native方法返回类型是jint的,可以直接返回int。
接下来看Java引用数据类型的转换。
(2)引用数据类型的转换
引用数据类型的转换如表2-2所示:
表2-2 Java引用数据类型转换关系表
Java引用类型 |
Native类型 |
Java引用类型 |
Native类型 |
All objects |
jobject |
char[] |
jcharArray |
java.lang.Class实例 |
jclass |
short[] |
jshortArray |
java.lang.String实例 |
jstring |
int[] |
jintArray |
Object[] |
jobjectArray |
long[] |
jlongArray |
boolean[] |
jbooleanArray |
float[] |
floatArray |
byte[] |
jbyteArray |
double[] |
jdoubleArray |
java.lang.Throwable实例 |
jthrowable |
|
|
由上表可知:
· 除了Java中基本数据类型的数组、Class、String和Throwable外,其余所有Java对象的数据类型在JNI中都用jobject表示。
JNIEnv是一个和线程相关的,代表JNI环境的结构体。
JNIEnv实际上就是提供了一些JNI系统函数。通过这些函数可以做到:
· 调用Java的函数。
· 操作jobject对象等很多事情。
JNIEnv,是一个和线程有关的变量。也就是说,线程A有一个JNIEnv,线程B有一个JNIEnv。由于线程相关,所以不能在线程B中使用线程A的JNIEnv结构体。读者可能会问,JNIEnv不都是native函数转换成JNI层函数后由虚拟机传进来的吗?使用传进来的这个JNIEnv总不会错吧?是的,在这种情况下使用当然不会出错。不过当后台线程收到一个网络消息,而又需要由Native层函数主动回调Java层函数时,JNIEnv是从何而来呢?根据前面的介绍可知,我们不能保存另外一个线程的JNIEnv结构体,然后把它放到后台线程中来用。这该如何是好?
还记得前面介绍的那个JNI_OnLoad函数吗?它的第一个参数是JavaVM,它是虚拟机在JNI层的代表,代码如下所示:
//全进程只有一个JavaVM对象,所以可以保存,任何地方使用都没有问题。
jint JNI_OnLoad(JavaVM* vm, void* reserved)
正如上面代码所说,不论进程中有多少个线程,JavaVM却是独此一份,所以在任何地方都可以使用它。那么,JavaVM和JNIEnv又有什么关系呢?答案如下:
· 调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数了。
· 另外,后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源。
操作对象jobject
成员变量和成员函数是由类定义的,它是类的属性,所以在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。
调用method:
NativeType Call
示例:
/*
调用JNIEnv的CallVoidMethod函数,注意CallVoidMethod的参数:
第一个是代表MediaScannerClient的jobject对象,
第二个参数是函数scanFile的jmethodID,后面是Java中scanFile的参数。
*/
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,
lastModified, fileSize);
获取成员:
//获得fieldID后,可调用Get
NativeType Get
//或者调用Set
void Set
//下面我们列出一些参加的Get/Set函数。
GetObjectField() SetObjectField()//jstring,jobject,jthrow等类型成员都可以使用该方法进行访问
GetBooleanField() SetBooleanField()
GetByteField() SetByteField()
GetCharField() SetCharField()
GetShortField() SetShortField()
GetIntField() SetIntField()
GetLongField() SetLongField()
GetFloatField() SetFloatField()
GetDoubleField() SetDoubleField()
jstring介绍
获取Java对象中的String成员:
jfieldID fid = env->GetFieldID(clazz,"mStr","Ljava/lang/String;");
jstring js = (jstring)env->GetObjectField(instance,fid);
Java中的String也是引用类型,不过由于它的使用非常频繁,所以在JNI规范中单独创建了一个jstring类型来表示Java中的String类型。虽然jstring是一种独立的数据类型,但是它并没有提供成员函数供操作。相比而言,C++中的string类就有自己的成员函数了。那么该怎么操作jstring呢?还是得依靠JNIEnv提供的帮助。这里看几个有关jstring的函数:
· 调用JNIEnv的NewString(JNIEnv *env, const jchar*unicodeChars,jsize len),可以从Native的字符串得到一个jstring对象。其实,可以把一个jstring对象看成是Java中String对象在JNI层的代表,也就是说,jstring就是一个Java String。但由于Java String存储的是Unicode字符串,所以NewString函数的参数也必须是Unicode字符串。
· 调用JNIEnv的NewStringUTF将根据Native的一个UTF-8字符串得到一个jstring对象。在实际工作中,这个函数用得最多。
· 上面两个函数将本地字符串转换成了Java的String对象,JNIEnv还提供了GetStringChars和GetStringUTFChars函数,它们可以将Java String对象转换成本地字符串。其中GetStringChars得到一个Unicode字符串,而GetStringUTFChars得到一个UTF-8字符串。
· 另外,如果在代码中调用了上面几个函数,在做完相关工作后,就都需要调用ReleaseStringChars或ReleaseStringUTFChars函数对应地释放资源,否则会导致JVM内存泄露。这一点和jstring的内部实现有关系,读者写代码时务必注意这个问题。
为了加深印象,来看processFile是怎么做的:
[-->android_media_MediaScanner.cpp]
static void
android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz, jstring path, jstring mimeType, jobject client)
{
MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz,fields.context);
......
//调用JNIEnv的GetStringUTFChars得到本地字符串pathStr
constchar *pathStr = env->GetStringUTFChars(path, NULL);
......
//使用完后,必须调用ReleaseStringUTFChars释放资源
env->ReleaseStringUTFChars(path, pathStr);
......
}
垃圾回收
我们知道,Java中创建的对象最后是由垃圾回收器来回收和释放内存的,可它对JNI有什么影响呢?下面看一个例子:
[-->垃圾回收例子]
static jobject save_thiz = NULL; //定义一个全局的jobject
static void
android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz, jstring path,
jstringmimeType, jobject client)
{
//保存Java层传入的jobject对象,代表MediaScanner对象
save_thiz = thiz;
return;
}
//假设在某个时间,有地方调用callMediaScanner函数
void callMediaScanner()
{
//在这个函数中操作save_thiz,会有问题吗?
}
因为和save_thiz对应的Java层中的MediaScanner很有可能已经被垃圾回收了,也就是说,save_thiz保存的这个jobject可能是一个野指针,如使用它,后果会很严重。
将一个引用类型进行赋值操作,它的引用计数不会增加,而垃圾回收机制只会保证那些没有被引用的对象才会被清理。但如果在JNI层使用下面这样的语句,是不会增加引用计数的。
save_thiz = thiz; //这种赋值不会增加jobject的引用计数。
JNI规范已很好地解决了这一问题,JNI技术一共提供了三种类型的引用,它们分别是:
· Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference。它包括函数调用时传入的jobject、在JNI层函数中创建的jobject。LocalReference最大的特点就是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。
· Global Reference:全局引用,这种对象如不主动释放,就永远不会被垃圾回收。
· Weak Global Reference:弱全局引用,一种特殊的GlobalReference,在运行过程中可能会被垃圾回收。所以在程序中使用它之前,需要调用JNIEnv的IsSameObject判断它是不是被回收了。
平时用得最多的是Local Reference和Global Reference,下面看一个实例,代码如下所示:
[-->android_media_MediaScanner.cpp::MyMediaScannerClient构造函数]
MyMediaScannerClient(JNIEnv *env, jobjectclient)
: mEnv(env),
//调用NewGlobalRef创建一个GlobalReference,这样mClient就不用担心被回收了。
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
......
}
//析构函数
virtual ~MyMediaScannerClient()
{
mEnv->DeleteGlobalRef(mClient);//调用DeleteGlobalRef释放这个全局引用。
}
每当JNI层想要保存Java层中的某个对象时,就可以使用Global Reference,使用完后记住释放它就可以了。
-----------------------------------------------------------------------------------------------------------------------------------
创建对象:注意构造函数名为
jclass jc = env->FindClass("com/smile/jnitest/JNITest$TestPoint");
//获取构造函数ID(构造函数函数名为,第三个参数为签名,意指传参和返回的对应缩写)
jmethodID mid = env->GetMethodID(jc,"","()V");
jobject testPoint = env->NewObject(jc,mid );
----------------------------------------------------------------------------------------------------------------------------------------
nonVirtual应该就是忽略了继承的多态性,即访问的方法,不是实例实际类型的方法,而是引用类型的方法。下面是一个类型:
public class Father {
@Override
public void fun() {
// TODO Auto-generated method stub
Log.d("333", "Father involked");
}
}
public class Child extends Father{
@Override
public void fun() {
// TODO Auto-generated method stub
Log.d("333", "Child involked");
}
}
在如果定义
Father instance = new Child();
在C++中如下调用
jobject fObj = env->GetObjectField(obj,fID);
jclass fclass=env->FindClass("lc/test/jni/Father");
jmethodID fm= env->GetMethodID(fclass,"fun","()V");
env->CallNonvirtualVoidMethod(fObj,fclass,fm);
Calling Instance Methods of a Superclass(调用实例超类的方法)
You can call instance methods defined in a superclass that have been overridden in the class to which the object belongs. The JNI provides a set of CallNonvirtual
functions for this purpose. To call instance methods from the superclass that defined them, you do the following:
GetMethodID
, as opposed to GetStaticMethodID)
.CallNonvirtualVoidMethod
, CallNonvirtualBooleanMethod
, and so on.It is rare that you will need to invoke the instance methods of a superclass. This facility is similar to calling a superclass method, say f
, using:
super.f();
in Java.
顺带说下 FindClass( ) 和 getObjectClass( )的区别,因为在写上面的测试代码的时候我出过这个错
FindClass( ) 就是通过包名类名去找,这个相当于绝对路径吧
getObjectClass( ) 是通过一个obj的类型去找,这个地方需要注意的是,他是通过对象类型去找,不是通过引用类型去找
比如一开始在java 里面写了
Father p=new Child();
那么在C++里面获得这个obj之后,如果用GetObjectClass( ) 获得的就是 Child 的 Class , 不是 Father 的 Class
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
GetStringUTFChars/ReleaseStringUTFChars和Get
其中GetStringUTFChars有一个isCopy的参数,代表是否复制一份在native堆,但是不知道有什么用,反正返回的都是一个const char*,就是说无法改变其内容的指针。这个const char*在被调用ReleaseStringUTFChars前有效。
Get
mode | actions |
---|---|
0 |
copy back the content and free the elems buffer |
JNI_COMMIT |
copy back the content but do not free the elems buffer |
JNI_ABORT |
free the buffer without copying back the possible changes |
经过实践证明,无论Get
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
JNI参考指南
本人另一篇可以用于学习jni的文章:SQLite--SQLiteDatabase、SQLiteOpenHelper、sqlite3.c--(jni、头文件)--源码分析基于Android M