深入理解Android卷一笔记

一、JNI调用流程分析

1.Java层

示例代码:

1
2
3
4
5
6
7
8
public class MediaScanner {
static {
System.loadLibrary("media_jni");
native_init();
}
private static native void native_init();
}
  • 加载对应的JNI库。
  • 声明native函数。

2.JNI层

示例代码:

1
2
3
4
//cpp
static void android_media_MediaScanner_native_init(JNIEnv *env) {
...
}

那么Java层调用native函数,系统是如何找到JNI层的对应函数的呢?

一、静态注册

流程:

  • 编写java层native函数声明,并javac编译为class文件。
  • 使用javah -o output packagename.classname生成.h头文件。
  • JNI层cpp或者c文件实现该头文件。

当Java层第一次调用该native方法时,jvm会从对应库找对应的头文件。如果找到,保存这个函数指针供下次调用;如果没找到就抛异常。

弊端:

  • 修改Java层的native方式后需要重新生成头文件,因为函数签名可能改变了。
  • 初次调用native函数才会去建立关联,有懒加载特性,有利有弊。

二、动态注册

JNI中有一个数据结构专门来记录这种一一对应关系的。

1
2
3
4
typedef struct {
const char* signature;
void* fnPtr;
} JNINativeMethod;

AndroidRunTime.cpp提供了registerNativeMethods方法来注册;其中又通过JNIHelp.c的jniRegisterNativeMethods。

1
2
jclass clazz = (*env) -> FindClass(env, className);
(*env) -> RegisterNatives(env, clazz, gMethods, numMethods);
  • 1.当Java层通过System.loadLibrary加载so库时,会先找是否有JNI_OnLoad函数,如果有就调用它;
  • 2.动态注册调用一般写在这个函数里。

提示:
Android提供了JNIHelp.h这个文件,它内部包含了jni.h,所以我们编写jni时可以直接引入JNIHelp.h。

4.什么是JNIEnv?

JNIEnv是一个与线程相关的代表JNI环境的结构体。我们经常通过它来从native层回调java层函数,但由于它是线程相关的,所以我们不能在B线程调用A线程的JNIEnv。

5.什么是JavaVM?

JavaVM顾名思义,就是一个java虚拟机变量,可以比作进程。不管有多少个线程,单进程就单独一个JavaVM。在JNI_OnLoad函数可以获得JavaVM。

  • 调用JavaVM->AttachCurrentThread可以获得当前线程的JNIEnv,这样可以在子线程回调java层函数。
  • 在线程结束时,调用JavaVM->DetachCurrentThread来释放相关资源。

6.什么是jfieldID和jmethodID?

1.获取:

1
2
3
//JNIEnv方法
jfieldID GetFieldID (jclass clazz, const char* name, const char* sig);
jmethodID GetMethodID (jclass clazz, const char* name, const char* sig);

建议在初始化的时候获取并保存下来,提高下一次调用的运行效率。

2.使用:

1
2
3
4
5
6
//JNIEnv方法
NativeType Call<type>Method (jobject jobj, jmethodID methodID, ...);
NativeType CallStatic<type>Method (jmethodID methodID, ...);
NativeType Get<type>Field (jobject jobj, jfieldID fieldID);
void Set<type>Field (jobject jobj, jfieldID fieldID, NativeType value);

7.jstring

1.java层String与native层string的转换关系:
jstring

2.释放资源:

1
2
3
//JNIEnv方法
void ReleaseStringChars(jstring str, char* chs);
void ReleaseStringUTFChars(jstring str, char* chs);

转换字符串和释放资源方法应该是成对出现的。

8.JNI类型签名

命令:javap -s -p xxx.class

意义:

  • -s 表示输出内部数据类型的签名信息。
  • -p 表示打印所有函数和成员的签名信息。默认只打印public级别。

9.引用

类型:

1
2
3
1.Local Reference 本地变量
2.Global Reference 全局变量(强印用)
3.Weak Global Reference 弱全局变量

使用:

1
2
3
4
5
6
7
//JNIEnv方法
NewGlobalRef(NativeType ref);
NewWeakGlobalRef(NativeType ref);
DeleteLoaclRef(NativeType ref);
DeleteGlobalRef(NativeType ref);
DeleteWeakGlobalRef(NativeType ref);

有时候我们可能创建太多LocalReference,这样内存占用可观,可以在方法结束调用前,提前手动回收LocalReference。

10.异常

JNI层可以截获和修改异常。

1
2
3
4
//JNIEnv方法
ExceptionOccured 判断是否发生异常
ExceptionClear 清空当前JNI发生的异常
ThrowNew 向java层抛异常