一、JNI原理
1.java加载本地so库的过程:
(1)在java类中加载本地.so库文件,并声明native方法;(native方法就是C/C++方法)
(也就是在java层只需要加载对应的JNI库和声明由关键字native修饰的函数)
(2)在C代码中实现so库中对应的方法;
(3)在java层需要调用native方法的地方进行调用;
·从动态加载开始看:
动态加载so库:NDK就是用动态加载,动态加载 .so库并通过 JNI调用其 .so库封装好的方法,.so库一般是由 C/C++编译的,只能被反编译成汇编代码,很难被破解, 一般情况是把 .so库一并打包到APK内部, .so库也可以动态的从 SD卡或者网络中加载进来;
(1)Android中加载so库:调用load()或者loadLibrary()方法;(具体看Android中加载so)
·System.load(String filename)是指定动态库的完整路径名;System.loadLibrary(String libname)只会从指定lib目录下查找,并加上lib前缀和.so后缀;
·loadLibrary()会将xxx动态库的名字转换为libxxx.so,再从/data/app/packagename/lib/arm64或者/vendor/lib64或者/system/lib64等路径中查询对应的动态库
·System.loadLibrary()或System.load()都会调用BaseDexClassLoader的findLibrary,再到DexPathList
·在构造classloader时候都会传一个app的library路径,这就是app的so目录了(这个到类加载器去看BaseDexClassLoader.java和DexPathList.java)(具体看类加载器)
·这就说到类加载器了:
BaseDexClassLoader的findLibrary:源码链接
public class BaseDexClassLoader extends ClassLoader {
...
@UnsupportedAppUsage
private final DexPathList pathList;
...
public BaseDexClassLoader(String dexPath,
String librarySearchPath, ClassLoader parent, ClassLoader[] sharedLibraryLoaders,
ClassLoader[] sharedLibraryLoadersAfter,
boolean isTrusted) {
super(parent);
...
this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
...
}
@Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}
}
DexPathList.java:源码链接
public DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory) {
this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false);
}
//对DexPathList的初始化:一个是dexElements,一个是nativeLibraryPathElements
DexPathList(ClassLoader definingContext, String dexPath,
String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
...
this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
// save dexPath for BaseDexClassLoader
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions, definingContext, isTrusted);
//app目录的native库
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
//系统目录的native库
this.systemNativeLibraryDirectories =
splitPaths(System.getProperty("java.library.path"), true);
//记录所有的native库(包括app目录和系统目录的)
this.nativeLibraryPathElements = makePathElements(getAllNativeLibraryDirectories());
...
}
java层传递的参数会在jni进行一层转换
- 虚拟机调用一个方法的实现,它发现如果这是一个native方法,则使用Method对象中的nativeFunc函数指针对象调用:
(1)nativeFunc会指向一个解析方法;
(2)通过JNIEnv提供的注册方法,手动建立native方法与java方法的关系**(动态注册)**,此时nativeFunc就指向了java方法。 - 第一次调用native方法时,由于不知道其具体实现,就用resolveNativeMethod去查找,等找到后,就将nativeFunc替换为找到的函数,这样下次再用的时候就省去了查找的过程。
- 第一次从dex中加载类时,会加载类中的所有方法,对于native方法,会将nativeFunc赋值为ResolveNativeMethod(此方法中有两种查询方法)
如果是静态方法,首先确保所属的class已经初始化
(1)内部本地方法查询
内部本地方法表是一个集合,这个集合里包含了java语言和虚拟机本身用到的native方法
在这个集合的结构定义里有调用native方法数组,就可以调用所有的native方法了。
(2)动态链接库查询(System.loadLibrary()加载的so库)
如果so中有JNI_OnLoad方法,则执行该方法。可以在该方法中做一些初始化工作,还可以手动建立java类中的native方法和so中的native方法的对应关系。(动态注册)
java对于native方法调用最后都是通过dlsym获取相应函数指针执行
二、JNI方法注册机制:
(将java层调用的native方法和native层的实现方法关联起来)
1、JNI的注册时机
1)Android系统在启动过程中,先启动Kernel创建init进程------>由init进程fork第一个横穿Java和C/C++的进程,即Zygote进程------>Zygote启动过程中会在AndroidRuntime.cpp中的startVm创建虚拟机----->VM创建完成后,调用startReg完成虚拟机中的JNI方法注册。
AndroidRuntime.cpp中的startReg方法源码链接
/*
* Register android native functions with the VM.
*/
/*static*/ int AndroidRuntime::startReg(JNIEnv* env)
{
ATRACE_NAME("RegisterAndroidNatives");
/*
* This hook causes all future threads created in this process to be
* attached to the JavaVM.
*/
//设置线程创建方法为androidSetCreateThreadFunc
androidSetCreateThreadFunc((android_create_thread_fn) javaCreateThreadEtc);
ALOGV("--- registering native functions ---\n");
env->PushLocalFrame(200);
//进程JNI方法的注册
if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
//register_jni_procs就是循环调用gRegJNI数组成员所对应的方法。
env->PopLocalFrame(NULL);
return -1;
}
env->PopLocalFrame(NULL);
return 0;
}
其中gRegJNI数组有很多成员变量,每个成员变量都代表一个文件的jni映射。因为Android系统在启动的时候就注册了大量的JNI方法,就在这个数组中。这些JNI方法就是Android系统在启动的时候预注册好的。(就在AndroidRuntime.cpp中)
其中REG_JNI是一个宏定义,作用就是调用相应的方法。
2)另一种就是应用程序自己注册JNI(以MediaPlayer.java为例)源码链接
public class MediaPlayer extends PlayerBase
implements SubtitleController.Listener
, VolumeAutomation
, AudioRouting
{
...
static {
System.loadLibrary("media_jni");//通过loadLibrary加载动态库"media_jni",会自动将名称扩展为media_jni.so
native_init();//native方法
}
...
private static native final void native_init();//将native关键字加在native_init之前,就可以在java层直接使用native层的方法
...
其对应的JNI层文件在/frameworks/base/media/jni/中源码链接
可以看到android_media_MediaPlayer.cpp文件,其中就有对应的方法:也就是java层中的native_init()方法对应的native层中的android_media_MediaPlayer_native_init函数
static void
android_media_MediaPlayer_native_init(JNIEnv *env)
{
jclass clazz;
clazz = env->FindClass("android/media/MediaPlayer");
if (clazz == NULL) {
return;
}
fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
if (fields.context == NULL) {
return;
}
...
所以JNI有两种注册时机:
(1)在启动的时候注册,可以通过查询AndroidRuntime.cpp中的gRegJNI,看看是否存在对应的register方法;
(2)应用程序调用System.loadLibrary()方式自己注册JNI。
2.JNI的注册方法
·可以看到上述java层中的native_init()方法对应的native层中的函数就是android_media_MediaPlayer_native_init函数,那么它是如何知道的呢?
因为native_init函数位于android.media这个包中(路径),它的全路径名是android.media.MediaPlayer.native_init,而JNI层函数的名字是android_media_MediaPlayer_native_init。因为在Native语言中,符号“.”有着特殊的意义,所以JNI层需要把“.”换成“_”。也就是通过这种方式,native_init找到了JNI层的函数。
JNI函数的两种注册方式:
(1)静态注册:
根据函数名建立java方法和JNI函数间的对应关系,要求JNI层函数的名字必须遵循特定的格式
·过程:编写Java代码,编译生成.class文件----->使用Java的工具程序javah,如:javah –o output 包名.类名(class文件)---->生成一个native层头文件----->在头文件里,声明了对应的native层函数---->在native层将生成的.h 文件拷到文件夹内,并将头文件写入.c文件中---->在c文件中实现里面的函数即可。
这个头文件的名字一般都会使用包名_类名.h的样式
静态注册:Java层调用native_init函数时,会从对应JNI库Java_android_media_MediaPlayer_native_linit查找方法,如果找到,会为这个native_init和native层的函数建立关联关系,保存JNI层函数的函数指针。以后再调用native_init函数时,直接使用这个函数指针。
(2)动态注册:(以MediaPlayer为例)
在JNI技术中,有一种结构JNINativeMethod结构体用来记录这种关系:源码链接
typedef struct {
char *name;//java方法的名字
char *signature;//java方法的签名信息
void *fnPtr;//JNI中对应的方法指针
} JNINativeMethod;
(这里涉及到签名:因为Java支持函数重载,可以定义同名但不同参数的函数。因此只根据函数名,是无法找到具体函数的。JNI技术中就使用参数类型和返回值类型的组合,作为一个函数的签名信息,有了签名信息和函数名,就能找到具体的函数啦!)
MediaPlayer在native层对结构体的使用(java层和native层的一一对应)(在 android_media_MediaPlayer.cpp中)源码链接
static const JNINativeMethod gMethods[] = {
{
"nativeSetDataSource",
"(Landroid/os/IBinder;Ljava/lang/String;[Ljava/lang/String;"
"[Ljava/lang/String;)V",
(void *)android_media_MediaPlayer_setDataSourceAndHeaders
},
...
{
"native_init", "()V", (void *)android_media_MediaPlayer_native_init},
{
"native_setup", "(Ljava/lang/Object;Landroid/os/Parcel;)V",(void *)android_media_MediaPlayer_native_setup},
{
"native_finalize", "()V", (void *)android_media_MediaPlayer_native_finalize},
...
};
注册使用的方法:
static int register_android_media_MediaPlayer(JNIEnv *env)
{
return AndroidRuntime::registerNativeMethods(env,
"android/media/MediaPlayer", gMethods, NELEM(gMethods));
}
可以看到注册使用的时候调用了registerNativeMethods函数,该函数在AndroidRunTime类中:
源码链接
int AndroidRuntime::registerNativeMethods(JNIEnv* env,
const char* className, const JNINativeMethod* gMethods, int numMethods) {
return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}
可以看到这里又调用了jniRegisterNativeMethods方法:源码链接
static int jniRegisterNativeMethods(
JNIEnv* env, const char* className, const JNINativeMethod* methods,
int numMethods) {
using namespace android::jnihelp;
jclass clazz = env->FindClass(className);//寻找全路径,查找类名
...
int result = env->RegisterNatives(clazz, methods, numMethods);//实际调用JNIEnv的RegisterNatives函数完成注册
env->DeleteLocalRef(clazz);
if (result == 0) {
return 0;
}
// Failure to register natives is fatal. Try to report the corresponding
// exception, otherwise abort with generic failure message.
jthrowable thrown = env->ExceptionOccurred();
if (thrown != nullptr) {
struct ExpandableString summary;
ExpandableStringInitialize(&summary);
if (GetExceptionSummary(env, thrown, &summary)) {
// __android_log_print(ANDROID_LOG_FATAL, "JNIHelp", "%s",
// summary.data);
}
ExpandableStringRelease(&summary);
env->DeleteLocalRef(thrown);
}
// __android_log_print(ANDROID_LOG_FATAL, "JNIHelp",
// "RegisterNatives failed for '%s'; aborting...",
// className);
return result;
}
其中调用了RegisterNatives方法(在jin.h中)源码链接
jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,
jint nMethods) {
return functions->RegisterNatives(this,clazz,methods,nMethods);
}
functions是指向JNINativeInterface结构体指针,也就是调用:
struct JNINativeInterface {
...
jint (JNICALL *RegisterNatives)
(JNIEnv *env, jclass clazz, const JNINativeMethod *methods,
jint nMethods);
...
}
动态注册就是在Java层通过System.loadLibrary加载完动态库后,查找该so库中的JNI_OnLoad函数,如果有就调用它,动态注册就是在这里完成的。(也就是在JNI_OnLoad函数中完成的动态注册)。
这个过程完成了gMethods数组中的方法的映射关系,比如java层的native_init()方法,映射到native层的android_media_MediaPlayer_native_init()方法。
以MediaPlayer为例的话,其so库为上边说过的libmedia_jni.so,看这个so的JNI_OnLoad函数是如何实现的:(在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);
...
if (register_android_media_MediaPlayer(env) < 0) {
ALOGE("ERROR: MediaPlayer native registration failed\n");
goto bail;
}
...
/* success -- return valid version number */
result = JNI_VERSION_1_4;
bail:
return result;
}
三、JavaVM和JNIEnv
- JavaVM是java虚拟机在jni层的代表,一个进程只有一个JavaVM, 所有线程共享一个JavaVM。
- JNIEnv表示java调用native语言的环境,是一个指向全部jni方法的指针。只在创建它的线程有效,不能跨线程传递。不同线程JNIEnv彼此独立。
获取JavaVM
(1)通过JNIEnv获取JavaVM
(2)在加载动态链接库的时候,JVM会调用JNI_OnLoad(JavaVM* jvm, void* reserved)(如果定义了该函数),第一个参数会传入JavaVM指针。
获取JNIEnv
不同线程间的JNIEnv不同,JavaVM一个进程只有一个,java代码调用c++代码的时候jni给了一个JNIEnv变量,但是如果在c++中新创建了一个子线程用于处理任务,那么在新的子线程中不能使用前面的那个JNIEnv。如果子线程中想要使用JNIEnv,只能通过JavaVM得到一个新的JNIEnv,在新的子线程中使用。