图解JNI库加载原理

jni库的加载.png

本地库如何加载到虚拟机中

一般我们需要加载本地库的时候会调用以下方法

System.loadLibrary("native-lib");

从类图我们可以知道接下来会调用Runtime类的loadLibrary0方法,在这个方法里面会做两件事:

  1. 通过loader.findLibrary(libraryName)找到对应库的全路径
  2. 通过doLoad(filename, loader)加载库文件

找到对应库的全路径

ClassLoader类是个抽象类,它里面的findLibrary方法并没有具体实现,所以我们得找到实现该方法的具体子类,可以通过打印,如Log.d("ClassLoader",this.getClassLoader().toString());,从打印结果dalvik.system.PathClassLoader[DexPathList[[zip file ..."得知,实际的ClassLoderPathClassLoader。而PathClassLoader类中的findLibrary方法是调用的父类实现,那么来看看它的父类BaseDexClassLoader是如何实现的。

BaseDexClassLoader#findLibrary

内部实现就一句代码,这句代码就指向了真正的核心处理类DexPathList

return pathList.findLibrary(name);

DexPathList

找到findLibrary方法,如下分为两步:

  1. 通过c方法拼接对应的库文件名
String fileName = System.mapLibraryName(libraryName);
  1. 遍历nativeLibraryPathElements成员变量
for (Element element : nativeLibraryPathElements) {
    String path = element.findNativeLibrary(fileName);

    if (path != null) {
        return path;
    }
}

return null;

C中如何拼接的库文件名?

  1. 计算两个宏定义的字符长度
int prefix_len = (int) strlen(JNI_LIB_PREFIX);
int suffix_len = (int) strlen(JNI_LIB_SUFFIX);

这两个宏根据不同系统,定义不同,如在linux系统下是:

#define JNI_LIB_PREFIX "lib"
#define JNI_LIB_SUFFIX ".so"​
  1. 如果传入的库文件为null或字符长度超过240会报异常
if (libname == NULL) {
    JNU_ThrowNullPointerException(env, 0);
    return NULL;
}
if (len > 240) {
    JNU_ThrowIllegalArgumentException(env, "name too long");
    return NULL;
}
  1. 拼接前缀
cpchars(chars, JNI_LIB_PREFIX, prefix_len);
  1. 拼接库文件名
(*env)->GetStringRegion(env, libname, 0, len, chars + prefix_len);
  1. 拼接后缀
len += prefix_len;
cpchars(chars + len, JNI_LIB_SUFFIX, suffix_len);

nativeLibraryPathElements这个成员变量是在哪赋值的?

// Native libraries may exist in both the system and
// application library paths, and we use this search order:
//
//   1. This class loader's library path for application libraries (librarySearchPath):
//   1.1. Native library directories
//   1.2. Path to libraries in apk-files
//   2. The VM's library path from the system property for system libraries
//      also known as java.library.path
//
// This order was reversed prior to Gingerbread; see http://b/2933456.

之前打印ClassLoader时,也打印出了本地库的路径:

nativeLibraryDirectories=[
/data/app/com.project.zero.myjni-1/lib/x86, 
/data/app/com.project.zero.myjni-1/base.apk!/lib/x86, 
/data/app/com.project.zero.myjni-1/split_lib_dependencies_apk.apk!/lib/x86, /data/app/com.project.zero.myjni-1/split_lib_slice_0_apk.apk!/lib/x86, 
/data/app/com.project.zero.myjni-1/split_lib_slice_1_apk.apk!/lib/x86, 
/data/app/com.project.zero.myjni-1/split_lib_slice_2_apk.apk!/lib/x86, 
/data/app/com.project.zero.myjni-1/split_lib_slice_3_apk.apk!/lib/x86, 
/data/app/com.project.zero.myjni-1/split_lib_slice_4_apk.apk!/lib/x86, 
/data/app/com.project.zero.myjni-1/split_lib_slice_5_apk.apk!/lib/x86, 
/data/app/com.project.zero.myjni-1/split_lib_slice_6_apk.apk!/lib/x86, 
/data/app/com.project.zero.myjni-1/split_lib_slice_7_apk.apk!/lib/x86, 
/data/app/com.project.zero.myjni-1/split_lib_slice_8_apk.apk!/lib/x86, 
/data/app/com.project.zero.myjni-1/split_lib_slice_9_apk.apk!/lib/x86, 
/system/lib, 
/vendor/lib]

DexPathList类的构造方法的注释我们可以得知本地库可能存在于系统和应用程序库路径,它的搜索顺序如下:

  1. 这个类加载器的应用程序库的库路径,
    1.1. 本地库目录,例如/data/app/com.project.zero.myjni-1/lib/x86
    1.2. 在apk文件中的库的路径,例如/data/app/com.project.zero.myjni1/split_lib_dependencies_apk.apk!/lib/x86
this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
  1. 系统库系统属性中的虚拟机库路径,/system/lib/vendor/lib这两个库,其中/vendor/lib为手机厂商的库,这两个系统库路径也可以通过打印(System.getProperty("java.library.path")得到
this.systemNativeLibraryDirectories =
        splitPaths(System.getProperty("java.library.path"), true);

加载库文件

核心处理方法是\art\runtime\java_vm_ext.ccLoadNativeLibrary方法:

sym = library->FindSymbol("JNI_OnLoad", nullptr);
...
if (version == JNI_ERR) {
  StringAppendF(error_msg, "JNI_ERR returned from JNI_OnLoad in \"%s\"", path.c_str());
} else if (IsBadJniVersion(version)) {
  StringAppendF(error_msg, "Bad JNI version returned from JNI_OnLoad in \"%s\": %d",
              path.c_str(), version);
}

在我们要加载so库中查找JNI_OnLoad方法,如果没有系统就认为是静态注册方式进行的,直接返回true,代表so库加载成功,如果找到JNI_OnLoad就会调用JNI_OnLoad方法,JNI_OnLoad方法中一般存放的是方法注册的函数,所以如果采用动态注册就必须要实现JNI_OnLoad方法,否则调用java中声明的native方法时会抛出异常。

IsBadJniVersion

return version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 && version != JNI_VERSION_1_6;

JNI_OnLoad方法只能返回上述三个值之一。

动态注册的步骤

jni动态注册流程.png
  1. 动态注册的方法可以不按"package name"+"class name"+"method name"的方式命名
JNIEXPORT jstring JNICALL
dynamic_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
        ....
}
  1. 添加动态注册的方法数组,要按照JNINativeMethod这个结构体的数据结构来添加
static const JNINativeMethod methods[] = {
                {"stringFromJNI", "()Ljava/lang/String;", (void *) dynamic_stringFromJNI}
};
  1. 定义一个宏
#define NELEM(x)            (sizeof(x)/sizeof(*(x)))
  1. 定义动态注册方法,参考\libnativehelper\JNIHelp.cppjniRegisterNativeMethods方法
static jint registerNatives(JNIEnv *env) {
    jclass jclz = env->FindClass("com/project/zero/myjni/MainActivity");
    if (jclz == NULL) {
        LOGI("class is NULL");
        return JNI_FALSE;
    }

    if (env->RegisterNatives(jclz, methods, NELEM(methods)) < 0) {
        LOGI("RegisterNatives Error");
        return JNI_FALSE;
    }

    return JNI_OK;
}
  1. 定义JNI_OnLoad方法,参考\frameworks\base\media\jni\android_media_MediaPlayer.cppJNI_OnLoad方法
jint JNI_OnLoad(JavaVM *vm, void * /* reserved */) {
    JNIEnv *env = NULL;

    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        LOGI("ERROR: GetEnv failed\n");
        return -1;
    }
    assert(env != NULL);

    registerNatives(env);

    LOGI("JNI REGISTER SUCCESS");
    return JNI_VERSION_1_4;
}

你可能感兴趣的:(图解JNI库加载原理)