此文章为《深入理解Android卷 I》的读书笔记,笔者已经完成了卷一的第一遍阅读,第一遍时写下了一些笔记,现在开始第二遍阅读,借此记录自己的阅读笔记,其中涉及的代码已经参考7.0修改
1 加载动态库(.so)
在加载JNI库是只需要加载名字,实际加载会拓展成.so(
unix-like
) 或者.dll(windows
)
System.loadLibrary("media_jni");
1.1 声明native函数
使用Java关键字native
即表明该函数由JNI完成
private static native final void native_init();
1.2 何时加载
如果java要调用native函数,就必须通过一个位于JNI层的动态库才能做到,所以就必须要加载
原则上是在调用native函数钱,任何时候任何地方都可以加载,通常做法是在类的
static
语句中加载,通过System.loadLibrary方法就可以了。
1.3 总结
使用JNI需要完成两个任务就可以了
- 加载对应的JNI库
- 声明关键字native修饰的函数
2 JNI层的MediaScanner分析
MediaScanner
的JNI代码在(frameworks\base\media\jni\android_media_MediaScanner.cpp
)
Java层声明的native_init()
函数对应的JNI函数为android_media_MediaScanner_native_init(JNIEnv *env)
实现如下:
static void android_media_MediaScanner_native_init(JNIEnv *env)
{
jclass clazz = env->FindClass(kClassMediaScanner);
if (clazz == NULL) return;
fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
if (fields.context == NULL) return;
}
2.1 注册JNI函数
-
静态方法
根据函数名来找对应的JNI函数。这种方法需要Java的工具程序Javah参与:
- 编写Java代码编译成.class文件;
- 使用Javah,例(
javah -o output packagename.calssname
),这样他会生成output.h的JNI层头文件。其中packagename.classname
是Java代码编译后的.class文件,而在生成的output.h文件里,声明了对应JNI函数,只要实现里面的函数即可。一般头文件名字会使用packagename_class.h的样式,事例中提到的JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile
7.0源码中已经改为下面的注册方式
-
动态注册
JNI只一个叫JNINativeMethod的结构来记录java native和JNI函数的一一对应关系
libnativehelper/include/nativehelper/jni.h
typedef struct {
//Java中native函数的名字,不用携带包的路径。例如“native_init“。
const char* name;
//Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。
const char* signature;
void* fnPtr; //JNI层对应函数的函数指针,注意它是void*类型。
} JNINativeMethod;
2.1.1 动态注册的使用
以MediaScanner为例 (frameworks/base/media/jni/android_media_MediaScanner.cpp
)
建立对应关系:
//定义一个JNINativeMethod数组,其成员就是MediaService中所有native函数的一一对应关系。
static JNINativeMethod gMethods[] = {
......
{
"processFile" //Java中native函数的函数名。
//processFile的签名信息,签名信息的知识,后面再做介绍。
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void*)android_media_MediaScanner_processFile //JNI层对应函数指针。
},
......
{
"native_init",
"()V",
(void *)android_media_MediaScanner_native_init
},
......
};
注册:
// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp
int register_android_media_MediaScanner(JNIEnv *env)
{
//调用AndroidRuntime的registerNativeMethods函数,第二个参数表明是Java中的哪个类
return AndroidRuntime::registerNativeMethods(env, kClassMediaScanner, gMethods, NELEM(gMethods));
}
AndroidRunTime类提供了一个registerNativeMethods函数来完成注册工作,实现如下:frameworks/base/core/jni/AndroidRuntime.cpp
int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods){
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
方法继续调用了 jniRegisterNativeMethods()
libnativehelper/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);
}
//实际上是调用JNIEnv的RegisterNatives函数完成注册的
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;
}
2.1.2 JNI注册总结
完成动态注册只要两个函数就能完成:
-
jclass clazz = (*env)->FindClass(env, className);
env指向一个JNIEnv结构体,classname为对应的Java类名,由于
JNINativeMethod中使用的函数名并非全路径名,所以要指明是哪个类。 - 调用JNIEnv的RegisterNatives函数,注册关联关系。
(*env)->RegisterNatives(env, clazz, gMethods,numMethods);
当自己使用的时候,在什么时候什么地方调用?
- 当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数,如果有,就调用它,而动态注册的工作就是在这里完成的。
所以,如果想使用动态注册方法,就必须要实现JNI_OnLoad函数,只有在这个函数中,才有机会完成动态注册的工作。一些初始化操作也在这里完成。
2.1.3 注册实例
libmedia_jni.so
的JNI_OnLoad
函数在android_media_MediaPlayer.cpp
中实现
frameworks/base/media/jni/android_media_MediaPlayer.cpp
jint JNI_OnLoad(JavaVM* vm, void* /* reserved */){
//该函数的第一个参数类型为JavaVM,这可是虚拟机在JNI层的代表喔,每个Java进程只有一个这样的JavaVM
JNIEnv* env = NULL;
jintresult = -1;
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) goto bail;
... //很多注册其他jni函数的操作
//动态注册MediaScanner的JNI函数。
if(register_android_media_MediaScanner(env) < 0) goto bail;
.... //很多注册其他jni函数的操作
returnJNI_VERSION_1_4;//必须返回这个值,否则会报错。
}
2.2 数据类型转换
Java数据类型分为基本数据类型和引用数据类型两种,JNI层也是区别对待这二者的。
2.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位 |
需要注意对应的字长的变化,jchar在Native语言中是16位,占两个字节,这和普通的char占一个字节的情况完全不一样。
2.2.2 引用数据类型
Java引用类型 | native类型 |
---|---|
ALL objects | jobject |
java.lang.Class | jclass |
java.lang.String | jstring |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
java.lang.Throwable | ithrowable |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
由上表可知:
- 除了Java中基本数据类型的数组、Class、String和Throwable外,其余所有Java对象的数据类型在JNI中都用jobject表示。
示例
//Java层processFile有三个参数。
processFile(String path, StringmimeType,MediaScannerClient client);
//JNI层对应的函数,最后三个参数和processFile的参数对应。
android_media_MediaScanner_processFile(JNIEnv*env, jobject thiz,
jstring path, jstring mimeType, jobject client)
由上可发现:
- Java的String类型在JNI层对应为jstring。
- Java的MediaScannerClient类型在JNI层对应为jobject。
- Java中的processFile只有三个参数,JNI层对应的函数五个参数?
第二个参数jobject
代表Java层的MediaScanner
对象,如果Java层是static
函数的话,那么这个参数将是jclass
,表示是在调用哪个Java Class的静态函数。
3 JNIEnv介绍
JNIEnv是一个和线程相关的,代表JNI环境的结构体,Context
?上下文?
JNIEnv实际就是提供了一些JNI系统函数。通过这些函数可以做到:
- 调用Java的函数
- 操作jobject对象等很多事
线程相关:
线程A有一个JNIEnv
,线程B有一个JNIEnv
。由于线程相关,所以不能在线程B中使用线程A的JNIEnv
结构体。
当后台线程收到一个网络消息,而又需要由Native层函数主动回调Java层函数时,我们需要JNIEnv,但是我们不能保存一个JNIEnv,所以需要从javaVM获取,在JNI_Onload
中的第一个参数就是,它是虚拟机在JNI层的代表,进程所有。
- 调用JavaVM的AttachCurrentThread函数,就可得到这个线程的JNIEnv结构体。这样就可以在后台线程中回调Java函数了。
- 后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应的资源。再来看JNIEnv的作用。
4 JNIEnv操作jobject
4.1 jfieldID和jmethodID
在JNI中所有的Java对象都是jobject,需要知道属性和方法必须通过JNIEnv来获得。
在JNI中,用jfieldID
和jmethodID
来表示Java的类成员和函数
jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);
-
jclass clazz
当前类 -
const char*name
变量或者函数名 -
const char*sig
变量或者函数签名
4.2 使用jfeildID和jmethodID
4.2.1 使用jmethodID
普通方法:mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr,lastModified, fileSize)
通过JNIEnv输出的CallVoidMethod
,再把jobject
、jMethodID
和对应参数传进去,JNI层就能够调用Java对象的函数了!实际上JNIEnv输出了一系列类似CallVoidMethod
的函数,形式如下:NativeType Call<type>Method(JNIEnv *env,jobject obj,jmethodID methodID, ...)
type是对应Java函数的返回值类型。
静态方法:调用Java中的static函数,则用JNIEnv输出的CallStatic<Type>Method
系列函数。
4.2.2 使用jfeildID
获得fieldID后
调用Get<type>Field
系列函数获取jobject
对应成员变量的值。
NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)
调用Set<type>Field
系列函数来设置jobject
对应成员变量的值。
void Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)
5 jstring介绍
Java中的String也是引用类型,不过由于它的使用非常频繁,所以在JNI规范中单独创建了一个jstring类型来表示Java中的String类型。需要JNIEnv提供的函数来操作:
- 调用JNIEnv的
NewString(JNIEnv *env, const jchar*unicodeChars,jsize len)
,可以从Native的字符串得到一个jstring对象。其实,可以把一个jstrin
g对象看成是Java中String
对象在JNI层的代表,也就是说,jstring
就是一个Java String。 - 调用JNIEnv的
NewStringUTF(const char* bytes)
将根据Native的一个UTF-8字符串得到一个jstring对象。在实际工作中,这个函数用得最多。 -
GetStringChars(jstring string, jboolean* isCopy)
和GetStringUTFChars(jstring string, jboolean* isCopy)
函数,它们可以将JavaString对象转换成本地字符串。 - 在代码中调用了上面几个函数,在做完相关工作后,就都需要调用
ReleaseStringChars(jstring string, const jchar* chars)
或ReleaseStringUTFChars(jstring string, const char* utf)
函数对应地释放资源,否则会导致JVM内存泄露。这一点和jstring的内部实现有关系。
6 JNI类型签名介绍
static JNINativeMethod gMethods[] = {
......
{
"processFile"
//processFile的签名信息
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void*)android_media_MediaScanner_processFile
},
......
}
Java中对应函数的签名信息,由参数类型和返回值类型共同组成。为什么需要这个签名信息呢?
- 因为Java支持函数重载,也就是说,可以定义同名但不同参数的函数。但仅仅根据函数名,是没法找到具体函数的。为了解决这个问题,JNI技术中就使用了参数类型和返回值类型的组合,作为一个函数的签名信息,有了签名信息和函数名,就能很顺利地找到Java中的函数了。
Java中函数定义:void processFile(String path, StringmimeType,MediaScannerClient client);
对应的JNI函数签名就是:(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V
括号内是参数类型的标示,最右边是返回值类型的标示,void类型对应的标示是V。
类型标示 | Java类型 |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
[I | int [] |
L/java/lang/String; | Stirng |
L/java/lang/Object; | object |
其他类似,对象后面需要加;号,如果数组还需要在前面加[
示例
函数签名 | Java函数 |
---|---|
()Ljava/lang/String; | String f() |
(ILjava/lang/Class;)J | long f(int i, Class c) |
([B)V | void f(byte[] bytes) |
7 垃圾回收
三种类型引用:
-
LocalReference
:本地引用。在JNI层函数中使用的非全局引用对象都是Local Reference
。它包括函数调用时传入的jobject、在JNI层函数中创建的jobject。LocalReference
最大的特点就是,一旦JNI层函数返回,这些jobject就可能被垃圾回收。 -
Global Reference
:全局引用,这种对象如不主动释放,就永远不会被垃圾回收。 -
Weak Global Reference
:弱全局引用,一种特殊的GlobalReference
,在运行过程中可能会被垃圾回收。所以在程序中使用它之前,需要调用JNIEnv的IsSameObject
判断它是不是被回收了。
平时用得最多的是Local Referenc
和Global Reference
.
每当JNI层想要保存Java层中的某个对象时,就可以使用Global Reference
,使用完后记住释放它就可以了。
JNIenv->New[xxxx]Ref()
创建引用,Delete[xxxx]Ref()
释放内存
示例
frameworks/base/media/jni/android_media_MediaScanner.cpp :: MyMediaScannerClient
class MyMediaScannerClient : public MediaScannerClient{
public:
MyMediaScannerClient(JNIEnv *env, jobject client)
:mEnv(env),
//调用NewGlobalRef创建一个GlobalReference,这样mClient就不用担心回收了。
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
...
}
virtual ~MyMediaScannerClient()
{
//调用DeleteGlobalRef释放这个全局引用。
mEnv->DeleteGlobalRef(mClient);
}
}
8 JNI中的异常处理
调用JNIEnv的某些函数出错后,会产生一个异常,但这个异常不会中断本地函数的执行,直到从JNI层返回到Java层后,虚拟机才会抛出这个异常。虽然在JNI层中产生的异常不会中断本地函数的运行,但一旦产生异常后,就只能做一些资源清理工作了JNI层函数可以在代码中截获和修改这些异常,JNIEnv提供了三个函数进行帮助:
-
ExceptionOccured()
函数,用来判断是否发生异常。 -
ExceptionClear()
函数,用来清理当前JNI层中发生的异常。 -
ThrowNew(jclass clazz, const char* message)
函数,用来向Java层抛出异常。
下一篇 《第三章 - init》读书笔记
网友评论