内容字号:默认大号超大号

段落设置:段首缩进取消段首缩进

字体设置:切换到微软雅黑切换到宋体

图解JNI库加载原理

2018-02-13 15:47 出处:清屏网 人气: 评论(0

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安卓开发

相关文章

发表评论愿您的每句评论,都能给大家的生活添色彩,带来共鸣,带来思索,带来快乐。

CopyRight © 2015-2016 QingPingShan.com , All Rights Reserved.

清屏网 版权所有 豫ICP备15026204号