通过MediaScanner理解JNI原理

参考链接:

《深入理解Android 卷I》

什么是JNI

JNI是Java Native Interface的缩写(Java本地调用),Java程序中的函数可以调用Native语言写的函数(一般指的是C/C++编写的函数),Native语言写的函数可以调用Java层的函数。

什么是JNI

Java语言的跨平台是因为在不同平台上可以运行Java虚拟机,而虚拟机是跑在具体平台上的,而本质上Java是通过JNI技术实现的跨平台,很多基层的模块在Java语言诞生之前已经有了比较优秀的实现,为了避免重复造轮子所以我们要使用JNI技术来使用已有的模块。JNI技术与Java应运而生,它也是推动Java快速发展的真正原因。

实现过程

在弄清楚JNI实现过程之前,先给大家介绍一个查看Android系统源码的网站

查看Android系统源码

接下来结合邓凡平的《深入理解Android 卷1》中的MediaScanner的案例来分析。

Java层

/frameworks/base/media/java/android/media/MediaScanner.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MediaScanner
{
//加载对应的so库
static {
System.loadLibrary("media_jni");
native_init();
}

//声明native方法
private native void processDirectory(String path, String extensions, MediaScannerClient client);
private native void processFile(String path, String mimeType, MediaScannerClient client);
public native void setLocale(String locale);

public native byte[] extractAlbumArt(FileDescriptor fd);

private static native final void native_init();
private native final void native_setup();
private native final void native_finalize();
}

可以看到MediaScanner.java中有很多native方法声明,这些方法的实现是被定义到动态库libmedia_jni.so中的。

JNI层对应的是libmedia_jni.so。media_jni就是JNI库的名字,下划线前的media是native库的名字,jni表示他是一个JNI库。Android平台默认采用“lib模块名_jni.so”的命名方式。

原则上来讲加载JNI库可以在使用native方法之前的任何时候任何地方加载,通常的做法是在static代码块中加载。

Native层

以上面processDirectory的native方法声明为例,打开native实现

/frameworks/base/media/jni/android_media_MediaScanner.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void
android_media_MediaScanner_processDirectory(
JNIEnv *env, jobject thiz, jstring path, jstring extensions, jobject ent)
{
MediaScanner *mp = (MediaScanner *)env->GetIntField(thiz, fields.context);

if (path == NULL) {
jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
return;
}
if (extensions == NULL) {
jniThrowException(env, "java/lang/IllegalArgumentException", NULL);
return;
}

const char *pathStr = env->GetStringUTFChars(path, NULL);
if (pathStr == NULL) { // Out of memory
jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
return;
}
const char *extensionsStr = env->GetStringUTFChars(extensions, NULL);
if (extensionsStr == NULL) { // Out of memory
env->ReleaseStringUTFChars(path, pathStr);
jniThrowException(env, "java/lang/RuntimeException", "Out of memory");
return;
}

MyMediaScannerClient myClient(env, client);
mp->processDirectory(pathStr, extensionsStr, myClient, ExceptionCheck, env);
env->ReleaseStringUTFChars(path, pathStr);
env->ReleaseStringUTFChars(extensions, extensionsStr);
}

android_media_MediaScanner_processDirectory对应的是上面的processDirectory方法,对应规则就是包名加类名并替换“.”为“_”

Java方法和Native方法的映射关系

注册JNI函数

当Java层调用native_init函数时,会从对应的JNI库中寻找Java_android_media_MediaScanner_native_init函数,如果没有会报错。如果找到会建立一个关联关系,其实就是保存JNI层的函数指针,后面再次调用该方法直接使用这个函数指针,保存关联关系是又虚拟机来完成的。

上面的MediaScanner通过对应规则建立的关系我们叫静态注册,这种注册有几个弊端:

  1. 所有native方法必须生成对应的javah头文件。
  2. javah生成的JNI层函数名特别长。
  3. 初次调用native的时候需要建立关联关系,会影响运行效率。

解决这几个弊端的方法就是动态注册,通过手动关联起来。用来记录这种一一对应关系的是JNINativeMethod结构。

JNINativeMethod结构体定义

1
2
3
4
5
typedef struct {
const char* name; //java中的native函数名
const char* signature; //java函数的签名信息,是参数和返回值类型的组合
void* fnPtr; //JNI层对应的函数指针,注意他是void*类型
} JNINativeMethod;

怎么使用这个结构体呢?

/frameworks/base/media/jni/android_media_MediaScanner.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int register_android_media_MediaScanner(JNIEnv *env)
{
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaScanner", gMethods, NELEM(gMethods));
}

static JNINativeMethod gMethods[] = {
{"processDirectory", "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void *)android_media_MediaScanner_processDirectory},
{"processFile", "(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
(void *)android_media_MediaScanner_processFile},
{"setLocale", "(Ljava/lang/String;)V", (void *)android_media_MediaScanner_setLocale},
{"extractAlbumArt", "(Ljava/io/FileDescriptor;)[B", (void *)android_media_MediaScanner_extractAlbumArt},
{"native_init", "()V", (void *)android_media_MediaScanner_native_init},
{"native_setup", "()V", (void *)android_media_MediaScanner_native_setup},
{"native_finalize", "()V", (void *)android_media_MediaScanner_native_finalize},
};

AndroidRuntime类提供了一个registerNativeMethods方法来完成动态注册工作。现在的问题是这个register_android_media_MediaScanner方法是什么时候被调用的呢?

当Java层通过System.loadLibrary加载完JNI动态库后,紧接着会查找库中一个叫JNI_OnLoad函数,如果有则调用它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
JNIEnv* env = NULL;
jint result = -1;

if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
LOGE("ERROR: GetEnv failed\n");
goto bail;
}
assert(env != NULL);

if (register_android_media_MediaScanner(env) < 0) {
LOGE("ERROR: MediaScanner native registration failed\n");
goto bail;
}

//....省略
}

可以看到在JNI_OnLoad中调用了register_android_media_MediaScanner方法来实现动态注册。

数据类型

Java和Native的基础数据类型之间的转换关系如下:

JavaNative符号属性字长
booleanjboolean无符号8bit
bytejbyte无符号8bit
charjchar无符号16bit
shortjshort无符号16bit
intjint有符号32bit
longjlong有符号64bit
floatjfloat有符号32bit
doublejdouble有符号64bit

Java和Native的引用类型之间的转换关系如下:

JavaNative
Object类型jobject
java.lang.Class实例jclass
java.lang.String实例jstring
Object[]jobjectArray
boolean[]jbooleanArray
byte[]jbyteArray
java.lang.Throwable实例jthrowable
char[]jcharArray
short[]jshortArray
int[]jintArray
long[]jlongArray
float[]jfloatArray
double[]jdoubleArray

除了java基本类型的数组、Class、String和Throwable外,其余数据类型在JNI中都用jobject表示。

1
private native void processFile(String path, String mimeType, MediaScannerClient client);

上面native方法对应的JNI的参数声明如下

1
2
3
4
5
6
static void
android_media_MediaScanner_processFile(
JNIEnv *env, jobject thiz, jstring path, jstring mimeType, jobject client)
{
//...
}

可以看到后面三个参数和native方法的String、MediaScannerClient对应。

JNIEnv对象

上面我们已经看到每个native方法所对应的JNI函数都有一个JNIEvent指针参数。

1
2
3
4
5
6
7
8
9
10
11
12
/*
* C++ object wrapper.
*
* This is usually overlaid on a C struct whose first element is a
* JNINativeInterface*. We rely somewhat on compiler behavior.
*/
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;

//...结构体内容太多,不方便贴出来
}

JNIEnv结构

JNIEnv是一个与线程相关的代表JNI环境的结构体。实际上是提供了一些JNI系统函数,通过这些函数可以做到调用Java的函数、操作jobject对象等。

通过JNIEnv操作jobject

一个Java对象是由成员属性和方法组成,所以在JNIEnv中定义了两个函数来分别对应Java中的属性和方法。

/dalvik/libnativehelper/include/nativehelper/jni.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct _JNIEnv {
/* do not rename this; it does not seem to be entirely opaque */
const struct JNINativeInterface* functions;

#if defined(__cplusplus)

//....

jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
{ return functions->GetFieldID(this, clazz, name, sig); }

jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
{ return functions->GetMethodID(this, clazz, name, sig); }

//....
}

jclass代表java类,name代表成员函数或成员变量的名字,sig为这个函数和变量的签名信息。

/frameworks/base/media/jni/android_media_MediaScanner.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)),
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
jclass mediaScannerClientInterface = env->FindClass("android/media/MediaScannerClient");
if (mediaScannerClientInterface == NULL) {
fprintf(stderr, "android/media/MediaScannerClient not found\n");
}
else {
mScanFileMethodID = env->GetMethodID(mediaScannerClientInterface, "scanFile",
"(Ljava/lang/String;JJ)V");
mHandleStringTagMethodID = env->GetMethodID(mediaScannerClientInterface, "handleStringTag",
"(Ljava/lang/String;Ljava/lang/String;)V");
mSetMimeTypeMethodID = env->GetMethodID(mediaScannerClientInterface, "setMimeType",
"(Ljava/lang/String;)V");
mAddNoMediaFolderMethodID = env->GetMethodID(mediaScannerClientInterface, "addNoMediaFolder",
"(Ljava/lang/String;)V");
}
}

通过上面的两个方法会获得对应的属性和方法的id(jmethodID和jfieldID),如果每次操作jobject前都去查询id将会影响效率,所以我们初始化的时候可以取出这些id并保存。

接下来看看如何调用scanFile方法。

1
2
3
4
5
6
7
8
9
// returns true if it succeeded, false if an exception occured in the Java code
virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
{
jstring pathStr;
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);
mEnv->DeleteLocalRef(pathStr);
return (!mEnv->ExceptionCheck());
}

可以看出来,通过JNIEnv的CallVoidMethod把jobject、jMethodID等对应参数传进去,JNI层就能调用Java函数了。

JNI类型签名

上面我们看到也多次提到sig为这个函数和变量的签名信息,为什么函数的参数需要这个信息呢?

因为Java支持函数重载,我们只是单纯的根据函数名无法确定是同一个函数,为了解决这个问题,JNI中将参数类型和返回值类型组合成了一个函数的签名信息,使用函数名和签名信息就可以唯一的确定一个函数。

常见的类型标识符如下:

类型标识Java类型
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble
L/java/languageString;String
[Iint[]
[L/java/lang/object;Object[]

如果Java类型是数组则标识符中会有一个“[”,另外引用类型标识符后面会有一个“;”

函数签名的小例子:

函数签名Java函数
()Ljava/lang/String;String f()
(ILjava/langClass;)Jlong f(int i, Class c)
([B)Vvoid f(byte[] bytes)

垃圾回收

在JNI中使用下面语句是不会增加引用计数的,所以使用的对象可能已经被回收。

1
save_this = thiz;

JNI中提供了三种类型的引用:

  1. Local Reference : 本地引用,一旦JNI层函数返回,这些object就可能被垃圾回收。
  2. Global Reference : 全局引用,不主动释放,永远不会被垃圾回收。
  3. Weak Global Reference : 弱全局引用,在运行过程中可能会被垃圾回收,使用前需要用JNIEnv的IsSameObject判断。

全局引用使用

1
2
3
4
5
6
7
8
9
MyMediaScannerClient(JNIEnv *env, jobject client)
: mEnv(env),
mClient(env->NewGlobalRef(client)), //使用NewGlobalRef创建全局引用
mScanFileMethodID(0),
mHandleStringTagMethodID(0),
mSetMimeTypeMethodID(0)
{
//....
}

在构造函数中创建了全局引用,在析构函数中释放全局引用。

1
2
3
4
virtual ~MyMediaScannerClient()
{
mEnv->DeleteGlobalRef(mClient); //释放全局引用
}

本地引用使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// returns true if it succeeded, false if an exception occured in the Java code
virtual bool scanFile(const char* path, long long lastModified, long long fileSize)
{
jstring pathStr;

//使用NewStringUTF创建了一个本地引用的String对象
if ((pathStr = mEnv->NewStringUTF(path)) == NULL) return false;

//当执行完CallVoidMethod方法后pathStr对象会被回收。
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize);

//既然上面说了pathStr会回收,这个DeleteLocalRef函数是不是多余的呢?
mEnv->DeleteLocalRef(pathStr);
return (!mEnv->ExceptionCheck());
}

上面代码清晰的展示了本地引用的使用过程,但是留下了一个疑问,我们试想假设scanFile方法被循环执行了10000次,那么pathStr是不是不能立即被回收,如果不主动调用DeleteLocalRef函数可能会造成内存泄漏。

总结

上面完整展示了JNI代码调用的过程,Java代码可以使用声明的native方法(未实现)来调用Native的实现,而Native方法可以通过JNIEnv对象的GetFieldID和GetMethodId方法来获取Java属性和方法来调用Java方法。对比一下会发现整个JNI的过程对Native层的要求比较高,Native层需要根据Java的native方法来注册并实现,而且还需要知道需要调用的Java方法的路径和名字来获取函数指针。

JNI职责关系

评论

Ajax Android AndroidStudio Animation Anroid Studio AppBarLayout Babel Banner Buffer Bulma ByteBuffer C++ C11 C89 C99 CDN CMYK COM1 COM2 CSS Camera Raw, 直方图 Chrome Class ContentProvider CoordinatorLayout C语言 DML DOM Dagger Dagger2 Darktable Demo Document DownloadManage ES2015 ESLint Element Error Exception Extensions File FileProvider Flow Fresco GCC Git GitHub GitLab Gradle Groovy HTML5 Handler HandlerThread Hexo Hybrid I/O IDEA IO ImageMagick IntelliJ Intellij Interpolator JCenter JNI JS Java JavaScript JsBridge Kotlin Lab Lambda Lifecycle Lint Linux Looper MQTT MVC MVP Maven MessageQueue Modbus Momentum MySQL NDK NIO NexT Next Nodejs ObjectAnimator Oracle VM Permission PhotoShop Physics Python RGB RS-232 RTU Remote-SSH Retrofit Runnable RxAndroid RxJava SE0 SSH Spring SpringBoot Statubar Style Task Theme Thread Tkinter UI UIKit UML VM virtualBox VS Code VUE ValueAnimator ViewPropertyAnimator Vue Vue.js Web Web前端 Workbench api apk bookmark by关键字 cli compileOnly computed css c语言 databases demo hexo hotfix html iOS icarus implementation init jQuery javascript launchModel logo merge methods mvp offset photos pug query rxjava2 scss servlet shell svg tkinter tomcat transition unicode utf-8 vector virtual box vscode watch webpack 七牛 下载 中介者模式 串口 临潼石榴 主题 书签 事件 享元模式 仓库 代理模式 位运算 依赖注入 修改,tables 光和色 内存 内核 内部分享 函数 函数式编程 分支 分析 创建 删除 动画 单例模式 压缩图片 发布 可空性 合并 同向性 后期 启动模式 命令 命令模式 响应式 响应式编程 图层 图床 图片压缩 图片处理 图片轮播 地球 域名 基础 增加 备忘录模式 外观模式 多线程 大爆炸 天气APP 太白山 头文件 奇点 字符串 字符集 存储引擎 宇宙 宏定义 实践 属性 属性动画 岐山擀面皮 岐山肉臊子 岐山香醋 工具 工厂模式 年终总结 开发技巧 异常 弱引用 恒星 打包 技巧 指令 指针 插件 插值 摄影 操作系统 攻略 故事 数据库 数据类型 数组 文件 新功能 旅行 旋转木马 时序图 时空 时间简史 曲线 杂谈 权限 枚举 架构 查询 标准库 标签选择器 样式 核心 框架 案例 桥接模式 检测工具 模块化 模板 模板引擎 模板方法模式 油泼辣子 泛型 洛川苹果 浅色状态栏 渲染 源码 源码分析 瀑布流 热修复 版本 版本控制 状态栏 状态模式 生活 留言板 相册 相对论 眉县猕猴桃 知识点 码云 磁盘 科学 笔记 策略模式 类图 系统,发行版, GNU 索引 组件 组合模式 绑定 结构 结构体 编码 网易云信 网格布局 网站广播 网站通知 网络 美化 联合 脚手架 膨胀的宇宙 自定义 自定义View 自定义插件 蒙版 虚拟 虚拟机 补码 补齐 表单 表达式 装饰模式 西安 观察者模式 规范 视图 视频 解耦器模式 设计 设计原则 设计模式 访问者模式 语法 责任链模式 贪吃蛇 转换 软件工程 软引用 运算符 迭代子模式 适配器模式 选择器 通信 通道 配置 链表 锐化 错误 键盘 闭包 降噪 陕西地方特产 面向对象 项目优化 项目构建 黑洞
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×