JNI是完善Java的一个重要功能,它让Java更加全面、封装了各个平台的差异性。
JNI在Android开发里的应用主要有:
-
音视频开发
-
热修复
-
插件化
-
逆向开发
-
…
为了更好的使用JNI,Android提供了NDK这个工具,NDK基于JNI,所以理解了JNI,NDK也会很容易掌握。通过学习JNI,可以把视野从Framework层深入到低层,但是因为在Android应用开发中基本不会用到JNI,所以这边以做理解为主。为之后的热修复插件化等做知识铺垫。
接下来以MediaRecorder框架的JNI为例,来了解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
来导包一样,这个成员方法同样的被我导入进来了。
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()
?它们是怎么对应的?
上述两个问题的答案就是注册,因为有了注册,所以产生了映射关系,让类与类、方法与方法有了联系,我们来看下他们是怎么关联的。
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的的注册,来实现函数的一一对应,这样做相比于静态注册:
- 直观明了
想要注册的方法全都放在一个数组里,统一管理并且一目了然
- 效率高
指针在一开始就指向了对应的函数,这样加快了效率。
==========================================================================
我们依旧拿上面 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中的 jint
、jstring
、jclass
这些数据类型是啥,我们就要搞清楚它们是这么来的。Java数据类型分成基本类型和引用类型,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 |
数组的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
=======================================================================
最后
那我们该怎么做才能做到年薪60万+呢,对于程序员来说,只有不断学习,不断提升自己的实力。我之前有篇文章提到过,感兴趣的可以看看,到底要学习哪些知识才能达到年薪60万+。
通过职友集数据可以查看,以北京 Android 相关岗位为例,其中 【20k-30k】 薪酬的 Android 工程师,占到了整体从业者的 30.8%!
北京 Android 工程师「工资收入水平 」
今天重点内容是怎么去学,怎么提高自己的技术。
1.合理安排时间
2.找对好的系统的学习资料
3.有老师带,可以随时解决问题
4.有明确的学习路线
当然图中有什么需要补充的或者是需要改善的,可以在评论区写下来,一起交流学习。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
object`
=======================================================================
最后
那我们该怎么做才能做到年薪60万+呢,对于程序员来说,只有不断学习,不断提升自己的实力。我之前有篇文章提到过,感兴趣的可以看看,到底要学习哪些知识才能达到年薪60万+。
通过职友集数据可以查看,以北京 Android 相关岗位为例,其中 【20k-30k】 薪酬的 Android 工程师,占到了整体从业者的 30.8%!
北京 Android 工程师「工资收入水平 」
[外链图片转存中…(img-cq26rLk4-1714503973147)]
今天重点内容是怎么去学,怎么提高自己的技术。
1.合理安排时间
2.找对好的系统的学习资料
3.有老师带,可以随时解决问题
4.有明确的学习路线
当然图中有什么需要补充的或者是需要改善的,可以在评论区写下来,一起交流学习。
[外链图片转存中…(img-cYxofEU6-1714503973147)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!