第一章,深入理解JNI
1.JNI概述
Java世界的虚拟机是用Native语言编写的,而虚拟机又运行在具体的平台上。有了JNI技术,就可以实现屏蔽不同操作系统平台,实现Java的平台无关性。安卓平台中JNI示意图:
2.学习JNI实例
这里通过在android studio中新建一个JNI工程,通过实际操作进行实例学习及分析
(1)加载JNI库
Java要调用native函数,就必须通过一个位于JNI层的动态库来实现,在调用nativa函数之前进行加载。通用做法是在类的static语句中加载,调用System.loadLibrary方法就可以。 Java中加载JNI库的函数一般用native关键字进行声明。
(2)JNI函数的注册
1)静态注册
以android studio中创建的demo工程为例:
MainActivity.java文件中的stringFromJNI函数:
/**
* A native method that is implemented by the 'native-lib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
对应于native-lib.cpp文件中就是 Java_cn_sensingai_jni_1learn_MainActivity_stringFromJNI函数:
/**
* 方法1:静态注册
* **/
Java_cn_sensingai_jni_1learn_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
观察可以发现native层cpp函数名对应的下划线“_”其实代表的就是文件的包名的路径:
上面所讨论的就是JNI函数注册的例子,“注册”的意思就是将Java层的native函数和JNI层对应的实现函数关联起来。有了这种关联,在Java层调用native函数时就可以顺利转到JNI层对应的函数执行了。上述所展示的通过函数名来对应Java函数名和native层函数名的方法就是静态注册方法。 它要求JNI函数名称遵循特定的格式。其建立联系的过程是通过Java虚拟机完成的。
2)动态注册
在JNI技术中,存在一个维护Java层与Native层的JNINativeMethod的结构,其定义如下:
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
上述结构体,name代表Java中native函数的名字,不用携带包的路径;signature代表Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合;fnPtr代表JNI层对应函数的指针,注意它为void*类型。
动态注册的工作,只需要如下两个关键函数来完成:
jclass clazz = env->FindClass(classname);
env->RegisterNatives(clazz, gMethods, numMethods))
使用上面两个函数便可以实现动态注册。那么整体调用流程是怎样的呢?首先,当Java层通过System.loadLibrary加载完JNI动态库之后,紧接着会查找库中一个叫JNI_Onload的函数,动态注册的两个函数就是在这里完成的。
这里贴下android studio里面实现的代码:
native-lib.cpp:
/**
* 方法2:动态注册
* **/
extern "C" JNIEXPORT jstring JNICALL
jni_getString(JNIEnv* env, jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
//jint jni_getInt(JNIEnv* env, jobject ob, jint a, jint b) {
// return a+b;
//}
static JNINativeMethod gMethods[] = {
{
"stringFromJNI",
"()Ljava/lang/String;",
(void *)jni_getString
},
};
JNIEXPORT jint
JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_6)) {
return JNI_ERR;
}
const char* classname = "cn/sensingai/jni_learn/MainActivity";
jclass clazz = env->FindClass(classname);
if (clazz == NULL) {
return JNI_ERR;
}
if (env->RegisterNatives(clazz, gMethods,
sizeof(gMethods) / sizeof(gMethods[0])) < 0) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
(3)数据类型转换
(1)基本数据类型转换
(2)引用数据类型转换
除了Java中基本数据类型的数组,Class,String和Throwable外,其余所有Java对象的数据类型在JNI中都用jobject表示。
(4)JNIEnv介绍
JNIEnv是一个与线程相关的代表JNI环境的结构体,下图展示了其内部结构。JNIEnv提供了JNI的许多系统函数,通过这些函数可以做到调用Java函数以及操作jobject对象等许多事情。
对于前面所介绍的JNIOn_Load函数:
jint JNI_OnLoad(JavaVM* vm, void* reserved)
其中,第一个参数JavaVM是虚拟机在JNI层的代表,不论进程中有多少线程,JavaVM独此一份,在任何地方都可以使用它。
JavaVM和JNIEnv之间的关系:
- 调用JavaVM的AttachCurrentThreead函数,就可以得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数了。
- 在后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应资源
这里在android studio中重现了安卓源码中关于MediaScanner的代码,实现了在JNI中对jobject的调用。
MediaScannerClient.java文件
package cn.sensingai.jni_learn;
public interface MediaScannerClient {
public void scanFile(String path, long lastModified, long fileSize,
boolean isDirectory, boolean noMedia);
/**
* Called by native code to return metadata extracted from media files.
*/
public void handleStringTag(String name, String value);
/**
* Called by native code to return mime type extracted from DRM content.
*/
public void setMimeType(String mimeType);
}
nativa-lib.cpp文件
/**
* JNI调用jObject
* **/
static const char* const kClassMediaScannerClient = "cn/sensingai/jni_learn/MediaScannerClient";
class MyMediaScannerClient {
public:
MyMediaScannerClient(JNIEnv* env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0) {
//先找到cn/sensingai/jni_learn/MediaScanner在JNI层中对应的实例
jclass mediaScannerClientInterface = env->FindClass(kClassMediaScannerClient);
mScanFileMethodID = env->GetMethodID(mediaScannerClientInterface,
"scanFile",
"(Ljava/lang/String;JJZZ)V");
mHandleStringTagMethodID = env->GetMethodID(mediaScannerClientInterface,
"handleStringTag",
"(Ljava/lang/String;Ljava/lang/String;)V");
mSetMimeTypeMethodID = env->GetMethodID(mediaScannerClientInterface,
"setMimeType",
"(Ljava/lang/String;)V");
}
virtual ~MyMediaScannerClient() {
mEnv->DeleteGlobalRef(mClient);
}
virtual bool scanFile(const char* path, long long lastModified,
long long fileSize, bool isDirectory, bool noMedia) {
jstring pathStr;
if((pathStr = mEnv->NewStringUTF(path)) == NULL) {
return false;
}
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
fileSize, isDirectory, noMedia);
mEnv->DeleteLocalRef(pathStr);
return (!mEnv->ExceptionCheck());
}
virtual bool handleStringTag(const char* name, const char* value) {
jstring nameStr, valueStr;
if ((nameStr = mEnv->NewStringUTF(name)) == NULL) {
return false;
}
if ((valueStr = mEnv->NewStringUTF(value)) == NULL) {
return false;
}
mEnv->CallVoidMethod(mClient, mHandleStringTagMethodID, nameStr, valueStr);
mEnv->DeleteLocalRef(nameStr);
mEnv->DeleteLocalRef(valueStr);
return (!mEnv->ExceptionCheck());
}
virtual bool setMimeType(const char* mimeType) {
jstring mimeTypeStr;
if ((mimeTypeStr = mEnv->NewStringUTF(mimeType)) == NULL) {
return false;
}
mEnv->CallVoidMethod(mClient, mSetMimeTypeMethodID, mimeTypeStr);
mEnv->DeleteLocalRef(mimeTypeStr);
return (!mEnv->ExceptionCheck());
}
private:
JNIEnv* mEnv;
jobject mClient;
jmethodID mScanFileMethodID;
jmethodID mHandleStringTagMethodID;
jmethodID mSetMimeTypeMethodID;
};
(5)JNI类型签名
在之前介绍的动态注册中有一段代码:
static JNINativeMethod gMethods[] = {
{
"stringFromJNI",
"()Ljava/lang/String;",
(void *)jni_getString
},
};
其中第二行的字符串 “()Ljava/lang/String;” 就是java中对应的签名信息,由参数类型和返回值类型共同组成。为什么需要签名信息呢?因为java函数支持函数的重载,也就是支持同名但是不同参数类型的函数。仅仅根据函数名称JNI没办法定位对应的java函数,因此需要签名信息。
具体格式:
eg:
此时Java函数名称:
那么JNI函数签名就是:
括号内就是参数类型的标识符,最右边是返回值类型的标识符,void对应的标识是V。常见的类型示意如下表:
小例子:
(6)垃圾回收
JNI提供了三种类型的引用:
- Local Reference:本地引用,在JNI层函数中使用的非全局引用对象都是Local Reference,它包括函数调用时传入的jobject和JNI层函数中创建的jobject。其特点是JNI函数返回,这些jobject就会被回收。
- Global Reference:全局引用,这种对象如果不主动释放,它永远不会被垃圾回收。
- Weak Global Reference:弱全局引用,是一种特殊的Global Reference,在运行过程中有可能会被回收,所以在使用它之前,需要用JNIEnv的IsSameObject判别其是否被回收了。
比如下面这段代码:
class MyMediaScannerClient {
public:
MyMediaScannerClient(JNIEnv* env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0) {
…………
}
virtual ~MyMediaScannerClient() {
mEnv->DeleteGlobalRef(mClient);
}
}
如上述代码,通过调用NewGlobalRef创建一个全局引用,这样就不会担心mClient被回收。最后再析构函数,需要对这个全局引用进行释放,否则会引起内存泄漏。
(7)JNI的异常处理
3.总结
主要掌握: