JNI原理学习,一个本科渣渣是怎么逆袭从咸鱼到Offer收割机的

比如:音视频开发涉及到的音视频编解码需要更快的处理速度,这就需要用到JNI

  • 为了整合一些以前的非Java语言开发的系统。

比如:需要用到早期实现的C/C++语言开发的一些功能或者系统,将这些功能整合到当前的系统或者新的版本中

JNI是完善Java的一个重要功能,它让Java更加全面、封装了各个平台的差异性。

JNI在Android开发里的应用主要有:

  • 音视频开发

  • 热修复

  • 插件化

  • 逆向开发

为了更好的使用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

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
img

本文在开源项目:Android开发不会这些?如何面试拿高薪! 中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…

PO等大厂,18年进入阿里一直到现在。**

深知大多数初中级安卓工程师,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Android移动开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-qkzAYjpT-1710843139696)]
[外链图片转存中…(img-m7u4d5Oo-1710843139697)]
[外链图片转存中…(img-RL3EnvNp-1710843139698)]
[外链图片转存中…(img-HlT6yTlE-1710843139698)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频
如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注Android)
[外链图片转存中…(img-hlfPxf9w-1710843139698)]

本文在开源项目:Android开发不会这些?如何面试拿高薪! 中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…

  • 13
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值