写点什么

JNI 原理学习,androidstudio 开发语言

作者:嘟嘟侠客
  • 2021 年 11 月 28 日
  • 本文字数:7002 字

    阅读完需:约 23 分钟

为了更好的使用 JNI,Android 提供了 NDK 这个工具,NDK 基于 JNI,所以理解了 JNI,NDK 也会很容易掌握。通过学习 JNI,可以把视野从 Framework 层深入到低层,但是因为在 Android 应用开发中基本不会用到 JNI,所以这边以做理解为主。为之后的热修复插件化等做知识铺垫。


接下来以 MediaRecorder 框架的 JNI 为例,来了解 JNI 的原理。


1.MediaRecorder 框架中的 JNI


=======================================================================================


MediaRecorder这个框架用于录音录像。这里不讲解这个框架,而是重点看这个框架中的 jni,来看下下图中的关系:



在 MediaRecorder 框架中,Java Framework 层对应的是MediaRecorder.java,是我们应用开发中直接调用的类。


JNI 层中对应的是 libmedia_jni.so,如果一个 so 库的名称结尾带有 _jni,则说明它是一个 JNI 的动态库。


Native 层中对应的是 libmedia.so,它是真正实现业务的类。


1.1 Java Framework 层的 MediaRecorder




我们先来查看 MediaRecorder.java 的源码,看看在 Java 层是如何使用 JNI 的:


// MediaRecorder.java


public class MediaRecorder


{


static {


// 1


System.loadLibrary("media_jni");


// 2


native_init();


}


....


private static native final voide native_init();


...


public native void start() throws IllegalStateException;


...


}


静态代码是最先加载的。


注释 1:加载名为 media_jni的 so 库。而真正的这个库名称就是 libmedia_jni,也就是加载这个库


注释 2:调用 native_init(),去看它的声明,发现它是一个 native 方法,也就是说它的实现是在 JNI 中。


为什么能无中生有一个 native_init()呢,是因为前面加载了一个 libmedia_jni库,这里面肯定封装了这个函数,我们加载进来的动作其实就和 import来导包一样,这个成员方法同样的被我导入进来了。


1.2 JNI 层的 MediaRecorder




MediaRecorder 的 JNI 层是写在 adnroid_media_MediaRecorder.cpp中的,它实现了 native_init()start(),在上面的代码中也有看到 start 方法:


// android_media_MediaRecorder.cpp


static void


android_media_MediaRecorder_native_init(JNIEnv *env)


{


jclass clazz;


clazz = env->FindClass("android/media/MediaRecorder");


if (clazz == NULL) {


return;


}


...


fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",


"(Ljava/lang/Object;IIILjava/lang/Object;)V");


if (fields.post_event == NULL) {


return;


}


}


static void


android_media_MediaRecorder_start(JNIEnv *env, jobject thiz)


{


...


}


android_media_Media_Recorder_native_init() 是 JavaFramwork 层native_init()在 JNI 层中的实现。


android_media_MediaRecorder_start() 则是start()的实现。


这里出现了疑问:


①:我是怎么通过在一开始的 media_jni这个 so 库找到 android_media_MediaRecorder.cpp这个 JNI 实现类?


②:我是怎么知道 native_init()的 JNI 实现就是 android_media_MediaRecorder_native_init()?它们是怎么对应的?


上述两个问题的答案就是注册,因为有了注册,所以产生了映射关系,让类与类、方法与方法有了联系,我们来看下他们是怎么关联的。


1.3 Native 方法注册




Native 方法注册分为两种:


  • 静态注册


多用于 NDK 开发


  • 动态注册


多用于 Framework 层开发


下面我们用实际的例子对两种注册做区分及了解。


1.静态注册


我们在 Android Studio 一个项目中


右键本项目->new->module->Java Library


LibraryName = media


Java class name = MediaRecorder.java,创建成功后,打开MediaRecorder.class,并学着 MediaRecorder 编写:



接着我们对MediaRecorder.java进行编译和生成 JNI 方法,我们进入 media/src/main/java 中,在 terminal 命令行中输入:


javac com/rikkatheworld/media/MediaRecorder.java


javah -jni com.rikkatheworld.media.MediaRecorder


最后会产生了一个 MediaReorder.class和一个 com_rikkatheworld_media_MediaRecorder.h头文件。


上面可能这里可能会有点坑,可以看我 Android音视频开发入门(5)使用LAME编码一个PCM文件第一节怎么去搭建一个 JNI 项目,这里我就不多赘述。


我们来看下这个被编译出来的头文件的内容:


// com_rikkatheworld_media_MediaRecorder.h


#include <jni.h>


#ifndef _Included_com_rikkatheworld_media_MediaRecorder


#define _Included_com_rikkatheworld_media_MediaRecorder


#ifdef __cplusplus


extern "C" {


#endif


// 1


JNIEXPORT void JNICALL Java_com_rikkatheworld_media_MediaRecorder_native_1init


(JNIEnv *, jclass);


JNIEXPORT void JNICALL Java_com_rikkatheworld_media_MediaRecorder_start


(JNIEnv *, jobject);


#ifdef __cplusplus


}


#endif


#endif


被编译出来的头文件就是我们 Java 层在 JNI 层中对应的类。


注释 1 中的方法:以 Java 开头,说明这个方法在 Java 中调用 JNI 方法的,后面的名称是以 包名+类名+方法名组成格式,还发现 native_1init()中多了个“1”,这是因为 Java 声明的native_init()中有 “_”下划线,转换成到 native 语言中,就变成了 “_1”。


其中 JNIEnv是 Native 世界中 Java 环境的代表,通过 JNIEnv *这个指针,就可以在 Native 层中访问 Java 层的代码并进行操作了,它只在创建它的线程中有效,不能跨线程传递。


jclass是 JNI 型数据,对应 Java 的 java.lang.Class 的实例,是所有对象的父类。


上面这两个数据类型是 JNI 层常用且关键的数据,后面会对其做介绍。


我们在 Java 层中调用 native_init()后,就会从 JNI 中寻找 Java_com_rikkatheworld_media_MediaRecorder_native_1init()的函数,如果没有,就会报错,如果找到就会为这两个方法建立一个关联,它就会保存一个 JNI 的函数指针,即一个指针指向了这个函数,等下一次再调用这个方法时,这个指针所指向的函数就能立马被调用。这就是静态注册。


静态注册总结:


根据方法名,将 Java 方法和 JNI 函数建立联系,它的缺点是:


  • JNI 层的函数名非常长,它是 Java+包名+类名+方法名

  • 声明 Native 方法的类需要用 javah 生成头文件

  • 第一次调用时,由于 JNI 层的指针还未指向对应函数,所以它要一个一个找直到找到,所以每第一次调用新的 Native 方法时,效率并不会太高


2.动态注册


静态注册是 Java 的 Native 通过方法指针来与 JNI 进行关联的,如果 Java 的 Native 方法一开始就知道它在 JNI 中对应的函数指针,就可以避免上述的缺点,这就是动态注册。


JNI 中有一种结构来记录 Java 的 Native 方法和 JNI 方法的关联联系,它就是 JNINativeMethod,它在jni.h中的源码是这样的:


// jni.h


typedef struct {


const char* name; //Java 方法中的名字


const char* signature; //Java 方法的简明信息


void* fnPtr; //JNI 中对应的方法指针


} JNINativeMethod;


我们应用开发中使用的 MediaRecorder 就是采用动态注册的方式,我们来看看其 JNI 层是怎么来使用这个 JNINativeMethod的:


// android_media_MediaRecorder.cpp


static const JNINativeMethod gMethods[] = {


...


{"start", "()V", (void *)android_media_MediaRecorder_start},


{"stop", "()V", (void *)android_media_MediaRecorder_stop},


{"pause", "()V", (void *)android_media_MediaRecorder_pause},


{"resume", "()V", (void *)android_media_MediaRecorder_resume},


{"native_reset", "()V", (void *)android_media_MediaRecorder_native_reset},


{"release", "()V", (void *)android_media_MediaRecorder_release},


{"native_init", "()V", (void *)android_media_MediaRecorder_native_init}...


};


在这个 JNI 文件中,定义了一个 JNINativeMethod数组,里面声明了这些关联的方法


比如最后一行的:


"native_init":在 Java 中的方法


"()v" :Java 层的签名


"android_media_MediaRecorder_native_init" :为对应 JNI 层的函数。


这里只声明一个 JNINativeMethod数组并不就完全的注册了这些函数,这个类还要去编写一个注册函数才有用,这个类中定义了一个函数来完成这个任务,它就是 register_android_media_MediaRecorder()


// android_media_MediaRecorder.cpp


// This function only registers the native methods, and is called from


// JNI_OnLoad in android_media_MediaPlayer.cpp


int register_android_media_MediaRecorder(JNIEnv *env)


{


return AndroidRuntime::registerNativeMethods(env,


"android/media/MediaRecorder", gMethods, NELEM(gMethods));


}


从英文注释中,可以看出这个方法用来注册 native 方法,并且它只在 JNI_OnLoad()中被调用,这个方法我们听名字就知道,肯定是在 System.loadLibrary()会调用其里面的这个方法。因为在多媒体框架中都要进行 JNINativeMethod数组的注册,因此,注册函数就统一放在 JNI_OnLoad()中。


也就是说 JNI_OnLoad()是在 android_mediaMediaPlayer.cpp中的,我们来看看这个方法:


// android_media_MediaPlayer.cpp


jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)


{


JNIEnv* env = NULL;


jint result = -1;


if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {


ALOGE("ERROR: GetEnv failed\n");


goto bail;


}


assert(env != NULL);


...


// 1


if (register_android_media_MediaRecorder(env) < 0) {


ALOGE("ERROR: MediaRecorder native registration failed\n");


goto bail;


}


...


result = JNI_VERSION_1_4;


bail:


return result;


}


JNI_OnLoad()中调用了整个多媒体框架的注册 JNINativeMethod()数组的函数。


除了上面说到的 MediaRecorder,还有我们熟知的 MediaPlayer、MediaScanner 等框架,都放在这里。


在注释 1 中就是调用了 register_android_media_MediaRecorder()


我们回到 register_android_media_MediaRecorder()中,它就是执行了 AndroidRuntime::registerNativeMethod()这个方法,我们看看这个方法:


// AndroidRuntime.cpp


/static/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,


const char* className, const JNINativeMethod* gMethods, int numMethods)


{


return jniRegisterNativeMethods(env, className, gMethods, numMethods);


}


它又返回了 jniRegisterNativeMethods(),它被定义在 JNIHelp.cpp中:


// JNIHelp.cpp


extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,


const JNINativeMethod* gMethods, int numMethods)


{


...


// 1


if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {


char* tmp;


const char* msg;


if (asprintf(&tmp, "RegisterNatives failed for '%s'; aborting...", className) == -1) {


msg = "RegisterNatives failed; aborting...";


} else {


msg = tmp;


}


e->FatalError(msg);


}


return 0;


}


注释 1 可以看出,最终通过调用JNIEnv.RegisterNatives()来完成 JNI 的注册。后面就不讲了,因为会扯到 JNIEnv,等了解了 JNIEnv,再去看这些代码也不迟。


动态注册总结:


通过声明一个 JNINativeMethod数组,来把对应的函数名称放到这个数组中,通过 JNIEnv 的的注册,来实现函数的一一对应,这样做相比于静态注册:


  • 直观明了


想要注册的方法全都放在一个数组里,统一管理并且一目了然


  • 效率高


指针在一开始就指向了对应的函数,这样加快了效率。


2.数据类型的转换


==========================================================================


我们依旧拿上面 MediaRecorder 为例,看下 android_media_MediaRecorder_start()


// adnroid_media_MediaRecorder.cpp


static void


android_media_MediaPlayer_start(JNIEnv *env, jobject thiz)


{


....


}


android_media_MediaRecorder_start() 的第二个参数 jobject,我们不是很清楚它是做什么的。


通过 JNI,我们在 Java 层的数据经过了转换,变成了我们看不懂的数据。


我们需要了解 JNI 中的 jintjstringjclass这些数据类型是啥,我们就要搞清楚它们是这么来的。Java 数据类型分成基本类型和引用类型,JNI 也分成了这两个部分。


2. 1 JNI 中的基本数据类型




基本数据类型的转换如下表所示,可以看到除了 void,其他数据类型只在前面加一个“j”,第三列是签名格式。之前在 JNINativeMethod中出现过,它就是一个类型的简写:


| Java | Native | Signature |


| --- | --- | --- |


| byte | jbyte | B |


| char | cchar | C |


| double | jdouble | D |


| float | jfloat | F |


| int | jint | I |


| short | jshort | S |


| long | jlong | J |


| boolean | jboolean | Z |


| void | void | V |


2. 2 JNI 中的引用数据类型




数组的 JNI 层数据都以 Array 结尾,签名都以“[”开头,有些数据类型以“;”结尾:


| Java | Native | Signature |


| --- | --- | --- |


| 所有对象 Object | jobject | L+classname +; |


| Class | jclass | Ljava/lang/Class; |


| Throwable | jthrowable | Ljava/lang/Throwable; |


| String | jstring | Ljava/lang/String |


| Object[] | jobject[]Array | [L+classname +; |


| byte[] | jbyteArray | [B |


| char[] | jcharArray | [C |


| double[] | jdoubleArray | [D |



他们也有继承关系,比如所有的类型都继承 jobject,所有的数组都继承自 jarray


再来看一个 MediaRecorder 的例子:


// MediaRecorder.java


private native void _setOutputFile(FileDescriptor fd) throws IllegalStateException, IOException;


// android_media_MediaRecorder.cpp


android_media_MediaRecorder_setNextOutputFileFD(JNIEnv *env, jobject thiz, jobject fileDescriptor)


{


...


}


可以看到 FileDescriptor就转成了 jobject


3.方法签名


=======================================================================


方法签名的意义是什么:便于注册后找到对应的函数


因为 Java 中有重载机制,即同返回类型+同类名,如果使用动态注册,在JNINativeMethod数组中,只放上两个名字(JNI 层和 JAVA 层函数名),到时候如果有重载函数,JNI 指针是无法区分的。


这个时候就出现了方法签名, 它会带上参数,比如 (jlong)V,就是输入的是一个 long 型,返回的是一个 void。


这样即使重载,也能通过参数来区分。这就是方法签名的意义。


Java 中用 javap来自动生成签名,这里就不再演示了。


4.JNIEnv


=========================================================================


JNIEnv 是只有 JNI 才有的,它是 Java 环境的代表,通过 JNIEnv *指针,就可以在 Native 层中访问 Java 层的代码。


它是线程私有的,so 每个线程都有自己的 JNIEnv *,它的作用有以下两点:


  1. 调用 Java 的方法

  2. 操作 Java 中的变量和对象


先来看下JNIEnv的定义,它在 jni.h 中被声明:


// jni.h


#if defined(__cplusplus)


//1 c++中 JNIEnv 类型


typedef _JNIEnv JNIEnv;


typedef _JavaVM JavaVM;


#else


//2 C 中 JNIEnv 类型


typedef const struct JNINativeInterface* JNIEnv;


typedef const struct JNIInvokeInterface* JavaVM;


#endif


首先先判断当前是什么语言,_cplusplus就是 C++,如果当前语言是 C++,JNIEnv 就是 _JNIEnv类型,如果是 C,就是 JNIInvokeInterface*这个类型。


这里的定义中我们也看到了 JVM,JavaVM就是 JVM 在 Native 层的代表。通过 JavaVM.AttachCurrentThread()可以获取这个线程的 JNIEnv,这样就可以在不同的线程调用 Java 方法了。


我们来看下 _JNIEnv这个 C++中定义的结构体:


// jni.h


struct _JNIEnv {


#if defined(__cplusplus)


...


jclass FindClass(const char* name)


{ return functions->FindClass(this, name); }


...


jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)


{ return functions->GetMethodID(this, clazz, name, sig); }


...


jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)


{ return functions->GetFieldID(this, clazz, name, sig); }


...


}


这里定义了很多的函数,之前讲过的动态注册就在这里面。


这里列出了三个常用的方法:


  1. FindClass


用来找到 Java 中指定名称的类


  1. GetMethodID


用来得到 Java 中的方法


  1. GetFieldID


用来得到 Java 中的成员变量。


这些函数都调用了 JNINativeInterface的方法,而JNINativeInterface对每个函数都保存了一个指针,这样就能够定位到虚拟机中的 JNI 函数表,从而实现了 JNI 层在虚拟机中的函数调用了。这样 JNI 就可以访问 Java 层的代码了。


4.1 jfieldID 和 jmethodID



最后

希望大家能有一个好心态,想进什么样的公司要想清楚,并不一定是大公司,我选的也不是特大厂。当然如果你不知道选或是没有规划,那就选大公司!希望我们能先选好想去的公司再投或内推,而不是有一个公司要我我就去!还有就是不要害怕,也不要有压力,平常心对待就行,但准备要充足。最后希望大家都能拿到一份满意的 offer !如果目前有一份工作也请好好珍惜好好努力,找工作其实挺累挺辛苦的。


这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司 19


《Android 学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享


年的面试题。把技术点整理成了视频和 PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。


由于篇幅有限,这里以图片的形式给大家展示一小部分。



本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

用户头像

嘟嘟侠客

关注

还未添加个人签名 2021.03.19 加入

还未添加个人简介

评论

发布
暂无评论
JNI原理学习,androidstudio开发语言