一、前言 在 Android 逆向过程中,常常可以见到 java 层使用 native 函数,但 native 函数并没有在 java 层实现,而是在 native 层实现,java 层通过加载库文件来正常使用该函数。java 层之所以可以使用 native 层实现的函数,是因为 JNI 的存在。
如果我们进一步探究 so 文件,可以发现有的 so 文件中含有 JNI_OnLoad 函数,而有的没有该函数。这个是因为函数组注册的方式不同,具体而言可分为静态注册和动态注册。
下面我们将一起学习 JNI 以及native函数的注册方法。
二、什么是JNI? JNI(Java Native Interface)译为 java 本地接口,是 java 与其他语言通信的桥梁。开发人员可以使用 JNI 技术来完成 java 编程无法处理的任务或者不便处理的任务,例如:
调用 Java 语言不支持的依赖于操作系统平台特性的功能。
整合以前的非 Java 语言开发的系统。
节省程序的运行时间,采用其他语言(如 C/C++)来提升运行效率。
三、Native注册方法 Native 方法注册可分为静态注册和动态注册,其中静态注册多用于 NDK 开发,而动态注册多用于 Framework 开发。
3.1 静态注册 Android Studio中创建一个native项目,会自动定义一个 JNI 函数stringFromJNI
:
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 33 34 35 36 package com.example.mynative;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.widget.TextView;import com.example.mynative.databinding.ActivityMainBinding;public class MainActivity extends AppCompatActivity { static { System.loadLibrary("mynative" ); } private ActivityMainBinding binding; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); TextView tv = binding.sampleText; tv.setText(stringFromJNI()); } public native String stringFromJNI () ; }
对应的stringFromJNI
方法在Native层的实现如下:
1 2 3 4 5 6 7 8 9 10 11 #include <jni.h> #include <string> extern "C" JNIEXPORT jstring JNICALL Java_com_example_mynative_MainActivity_stringFromJNI ( JNIEnv* env, jobject ) { std::string hello = "Hello from C++" ; return env->NewStringUTF (hello.c_str ()); }
对应的函数名为Java_com_example_mynative_MainActivity_stringFromJNI
,该函数名特点为:
带有JNIEXPORT
和JNICALL
两个宏定义。
符合函数命名规则:以字符串 “Java” 为前缀,用 “_” 下划线将包名、类名以及方法名连接起来。
当我们在 Java 层调用该函数时,就会根据以上特征从 JNI 中寻找对应函数,如果找到了,就会为 Java 层 和 Native 层的相应函数建关联,即保存 JNI 的函数指针,以后调用就可以直接使用函数指针即可。静态注册就是根据方法名,将Java方法和JNI函数建立关联,但是它有一些缺陷:
JNI层的函数名过长。
初次调用Native方法时需要建立关联,影响效率。
我们知道,静态注册就是Java的Native方法通过方法指针来与JNI进行关联,如果Java的Native方法知道它在JNI中对应的函数指针,就可以避免上述的缺点,这就是动态注册。
3.2 动态注册 在动态注册中,JNI 中会使用 JNINativeMethod 结构体来记录 Java 的Native 方法和 JNI 方法的关联关系,它的定义如下:
1 2 3 4 5 typedef struct { const char * name; const char * signature; void * fnPtr; }JNINativeMethod;
以 Android 系统中的 MediaRecorder 为例,来探究动态注册的过程。
1 2 3 4 5 6 7 8 9 10 11 12 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}, ... };
在 JNINativeMethod 类型的 gMethods 数组,存放的都是 MediaRecorder 的 Native 方法与 JNI 层函数的对应关系(即 JNINativeMethod 结构体)。
这里只是定义了 JNINativeMethod 类型的数组,还需要注册它,注册的函数为 register_android_media_MediaRecorder,这个函数会被 JNI_OnLoad 函数调用,而 JNI_OnLoad 函数是在 java 层调用 System.loadLibrary 函数时被调用。 MediaRecorder 对应的 JNI_OnLoad 函数如下:
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 jint JNI_OnLoad (JavaVM* vm, void * ) { 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 ); ... if (register_android_media_MediaPlayer (env) < 0 ) { ALOGE ("ERROR: MediaPlayer native registration failed\n" ); goto bail; } 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 数组的函数。跟随 register_android_media_MediaRecorder 函数进一步深入:
1 2 3 4 5 6 7 8 9 int register_android_media_MediaRecorder (JNIEnv *env) { return AndroidRuntime::registerNativeMethods (env, "android/media/MediaRecorder" , gMethods, NELEM (gMethods)); }
该函数中调用了 AndroidRuntime 的 registerNativeMethods 函数。
1 2 3 4 5 6 7 8 9 10 11 int AndroidRuntime::registerNativeMethods (JNIEnv* env, const char * className, const JNINativeMethod* gMethods, int numMethods) { return jniRegisterNativeMethods (env, className, gMethods, numMethods); }
调用了 jniRegisterNativeMethods 函数。
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 33 34 35 36 37 38 extern "C" int jniRegisterNativeMethods (C_JNIEnv* env, const char * className, const JNINativeMethod* gMethods, int numMethods) { JNIEnv* e = reinterpret_cast <JNIEnv*>(env); ALOGV ("Registering %s's %d native methods..." , className, numMethods); scoped_local_ref<jclass> c (env, findClass(env, className)) ; if (c.get () == NULL ) { char * tmp; const char * msg; if (asprintf (&tmp, "Native registration unable to find class '%s'; aborting..." , className) == -1 ) { msg = "Native registration unable to find class; aborting..." ; } else { msg = tmp; } e->FatalError (msg); } 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 ; }
最终通过调用 JNIEnv 的 RegisterNatives 函数来完成 JNI 的注册。 JNIEnv 在 JNI 中非常重要,下一小节将会讲述它。
四、数据类型的转换 Java层的数据类型分为两类:基本数据类型和引用数据类型,JNI层也做了相应的数据类型划分。
4.1 基本数据类型的转换
Java
Native
Signature
byte
jbyte
B
char
jchar
C
double
jdouble
D
float
jfloat
F
int
jint
I
short
jshort
S
long
jlong
J
boolean
jboolean
Z
void
void
V
可以看出,基本数据类型的转换,除了void类型以外,其他的数据类型都只要在前面加上”j”就行了。
4.2 引用数据类型的转换
Java
Native
Signature
所有对象
jobject
L + classname + ;
Class
jclass
Ljava/lang/Class;
String
jstring
Ljava/lang/String;
Throwable
jthrowable
Ljava/lang/Throwable;
Object[]
jobjectArray
[L + classname + ;
byte[]
jbyteArray
[B
char[]
jcharArray
[C
double[]
jdoubleArray
[D
float[]
jfloatArray
[F
int[]
jintArray
[I
short[]
jshortArray
[S
long[]
jlongArray
[J
boolean[]
jbooleanArray
[Z
其中jclass、jstring、jarray和jthrowable都继承自jobject,而jobjectArray、jbyteArray等都继承jarray。
五、方法签名 在前面两个表格中,都列举了数据类型的签名格式(Signature),方法签名就是由签名格式组成的。
举个例子:
1 2 3 4 5 6 7 static const JNINativeMethod gMethods[] = { ... {"native_init" , "()V" , (void *)android_media_MediaRecorder_native_init}, {"native_setup" , "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V" , (void *)android_media_MediaRecorder_native_setup}, ... };
每个方法数组中的第二个就是方法签名。Java中是可以定义重载方法,方法名相同但参数不同。正因如此,在JNI中仅仅通过方法名是无法找到Java中对应的具体方法的,JNI为了解决这一问题就将参数类型和返回值类型组合在一起作为方法签名。通过方法签名和方法名就可以找到对应的Java方法。JNI的方法签名格式如下:
六、解析 JNIEnv JNIEnv 是 Native 层中 Java 环境的代表,通过 JNIEnv* 指针就可以在 Native 层中访问 Java 层的代码并进行操作,比如调用 Java 的方法、操作 Java 的变量和对象等。但是它只在创建它的线程中有效,不能跨线程传递, 因此不同线程的 JNIEnv 是彼此独立的。
JNIEnv 的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 struct _JNIEnv ;struct _JavaVM ;typedef const struct JNINativeInterface * C_JNIEnv;#if defined(__cplusplus) typedef _JNIEnv JNIEnv;typedef _JavaVM JavaVM;#else typedef const struct JNINativeInterface * JNIEnv;typedef const struct JNIInvokeInterface * JavaVM;#endif
_JNIEnv
和 _JavaVM
结构体存放了各种函数的定义。这里使用了预定义宏 __cplusplus
来区分 C 和 C++两种代码。_JavaVM
是 Java
虚拟机在 JNI
层的代表,在一个虚拟机进程中只有一个 JVM
,因此该进程的所有线程都可以使用这个 JVM
。通过 JVM
的 AttachCurrentThread
函数可以获取这个线程的 JNIEnv
,这样就可以在不同的线程中调用 Java
方法了。
_JNIEnv
的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct _JNIEnv { const struct JNINativeInterface * functions;#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); } ... }
_JNIEnv
是一个结构体,其内部包含了 JNINativeInterface
。在_JNIEnv
中定义了许多函数,其中有三个比较常用的函数:
FindClass :根据参数name
获取Java
中的类。
GetMethodID :根据参数clazz
和参数name
获取Java
中的类中的方法。
GetFieldID :根据参数clazz
和参数name
获取Java
中的类中的成员变量。
这三个方法都调用了JNINativeInterface
中定义的函数,因此C/C++对应的JNIEnv
的类型都和JNINativeInterface
结构体有关,该结构体定义如下:
1 2 3 4 5 6 7 8 9 10 11 struct JNINativeInterface { ... jclass (*GetObjectClass)(JNIEnv*, jobject); ... jmethodID (*GetMethodID)(JNIEnv*, jclass, const char *, const char *); ... jfieldID (*GetStaticFieldID)(JNIEnv*, jclass, const char *, const char *); ... };
在JNINativeInterface
结构体中定义了很多和JNIEnv
结构体对应的函数指针。通过这些函数指针,就能够定位到虚拟机中的 JNI 函数表,从而实现 JNI 层在虚拟机中的函数调用,这样就 JNI 层就可以调用 Java 层的方法了。
七、引用类型 和Java的引用类型一样,JNI也有引用类型,它们分别是本地引用(Local References)、全局引用(Global References)和弱全局引用(Weak Global References)。
7.1 本地引用 JNIEnv提供的函数所返回的引用基本上都是本地引用,因此本地引用也是JNI中最常见的引用类型。本地引用具有以下特点:
当Native函数返回时,这个本地引用就会被自动释放。(也可以手动调用JNIEnv的DeleteLocalRef函数来删除本地引用)
只在创建它的线程中有效,不能够跨线程使用。
局部引用是JVM负责的引用类型,受JVM管理。
7.2 全局引用 全局引用与本地引用相反,它具有以下特点:
在native函数返回时不会被自动释放,因此全局引用需要手动释放,并且不会被GC回收。
全局引用是可以跨线程使用的。
全局引用不受到JVM管理。
全局引用通过JNIEnv的NewGlobalRef函数创建,通过JNIEnv的DeleteGlobalRef函数释放。示例代码如下:
1 2 3 jclass clazz = env->GetObjectClass(classname ) ; jcalss mClass = (jcalss)env->NewGlobalRef(clazz ) ; env->DeleteGlobalRef(mClass )
7.3 弱全局引用 弱全局引用是一种特殊的全局引用,它和全局引用的特点相似,但是弱全局引用是可以被GC回收的。弱全局引用被GC回收之后会指向NULL,因此在调用前需要判断它是否被回收了(通过JNIEnv的IsSameObject函数来判断)。弱全局引用通过JNIEnv的NewWeakGlobalRef函数来创建弱全局引用,通过JNIEnv的DeleteWeakGlobalRef函数释放。
示例代码如下:
1 2 3 4 5 6 7 8 9 jclass clazz = env->GetObjectClass(classname ) ; jcalss mClass = (jcalss)env->NewWeakGlobalRef(clazz ) ; env->DeleteWeakGlobalRef(mClass ) if (env->IsSameObject(mClass ,NULL) ){ }else { }
八、总结 JNI 是 Java 和其他语言通信的桥梁。JNI 注册又分为静态注册和动态注册。静态注册则是在方法被调用时将 Java 方法和JNI函数建立关联;而动态注册则是在方法被调用前就依靠 JNI_OnLoad 方法建立联系。但不管是哪种注册方式,它们在 Java 层的代码编写都是一样的。
Java 层能够调用 Native 层的函数,那反过来也同样可以。这需要依靠 JNIEnv 和 JavaVM,通过 JavaVM 获取当前线程的 JNIEnv,然后通过调用 JNIEnv 中的一系列函数就可以访问到 Java 层的类、方法、变量、对象等。
参考:
Android深入理解JNI(一)JNI原理与静态、动态注册 | BATcoder - 刘望舒 (liuwangshu.cn)
Design Overview (oracle.com)
Android NDK开发——静态注册和动态注册 - 知乎 (zhihu.com)