转载:深入理解 System.loadLibrary
本文主要讲述 Android 加载动态链接库的过程及其涉及的底层原理。
会先以一个Linux的例子描述native层加载动态链接库的过程,
再从Java层由浅入深分析System.loadLibrary
首先我们知道在Android(Java)中加载一个动态链接库非常简单,只需一行代码:
事实上这是Java提供的API,对于Java层实现基本一致,但是对于不同的JVM其底层(native)实现会有所差异。本文分析的代码基于Android 6.0系统。
看过《理解JNI技术》的应该知道上述代码执行过程中会调用native层的JNI_OnLoad方法,一般用于动态注册native方法。
# Linux 系统加载动态库过程分析
Android是基于Linux系统的,那么在Linux系统下是如何加载动态链接库的呢?
如果对此不敢兴趣或者对C++比较陌生的可以先跳到后面阅读Android Java层实现部分,但是最终还是会涉及到native代码。
Linux环境下加载动态库主要包括如下函数,位于头文件#include
可以通过下述命令可以查看上述函数的具体使用方法:
如何在Linux环境下生成动态链接库,如何加载并使用动态链接库中的函数?带着问题看个例子:
下面是一个简单的C++文件,作为动态链接库包含计算相关函数:
[caculate.cpp]
对于C++文件函数前的 extern “C” 不能省略,原因是C++编译之后会修改函数名,之后动态加载函数的时候会找不到该函数。加上extern “C”是告诉编译器以C的方式编译,不用修改函数名。
然后通过下述命令编译成动态链接库:
这样会在同级目录生成一个动态库文件:libcaculate.so
然后编写加载动态库并使用的代码:
[main_call.cpp]
主要就用了上面提到的4个函数,过程如下:
打开动态库,拿到一个动态库句柄
通过句柄和方法名获取方法指针地址
将方法地址强制类型转换成方法指针
调用动态库中的方法
通过句柄关闭动态库
中间会使用dlerror检测是否有错误。
有必要解释一下的是方法指针地址到方法指针的转换,为了方便这里定义了一个方法指针的别名:
指明该方法接受两个int类型参数返回一个int值。
拿到地址之后强制类型转换成方法指针用于调用:
最后只要编译运行即可:
因为代码中使用了c++11标准新加的特性,所以编译的时候带上-std=c++11,另外使用了头文件dlfcn.h需要带上-ldl,编译生成的main文件即是二进制可执行文件,需要将动态库放在同级目录下执行。
上面就是Linux环境下创建动态库,加载并使用动态库的全部过程。
由于Android基于Linux系统,所以我们有理由猜测Android系统底层也是通过这种方式加载并使用动态库的。下面开始从Android 上层 Java 代码开始分析。
# System.loadLibrary
[System.java]
此处VMStack.getCallingClassLoader()拿到的是调用者的ClassLoader,一般情况下是PathClassLoader。
[Runtime.java]
这里根据ClassLoader是否存在分了两种情况,当ClasssLoader存在的时候通过loader的findLibrary()查看目标库所在路径,当ClassLoader不存在的时候通过mLibPaths加载路径。最终都会调用doLoad加载动态库。
下面只讲ClassLoader存在的情况,不存在的情况更加简单。findLibrary位于PathClassLoader的父类BaseDexClassLoader中:
[BaseDexClassLoader.java]
其中pathList的类型为DexPathList,它的构造方法如下:
[DexPathList.java]
这里收集了apk的so目录,一般位于:/data/app/${package-name}/lib/arm/
还有系统的so目录:System.getProperty(“java.library.path”),可以打印看一下它的值:/vendor/lib:/system/lib,其实就是前后两个目录,事实上64位系统是/vendor/lib64:/system/lib64。
最终查找so文件的时候就会在这三个路径中查找,优先查找apk目录。
[DexPathList.java]
String fileName = System.mapLibraryName(libraryName)的实现很简单:
[System.java]
也就是为什么动态库的命名必须以lib开头了。
然后会遍历nativeLibraryPathElements查找某个目录下是否有改文件,有的话就返回:
[DexPathList.java]
回到Runtime的loadLibrary方法,通过ClassLoader找到目标文件之后会调用doLoad方法:
[Runtime.java]
这里的ldLibraryPath和之前所述类似,loader为空时使用系统目录,否则使用ClassLoader提供的目录,ClassLoader提供的目录中包括apk目录和系统目录。
最后调用native代码:
[java_lang_Runtime.cc]
继续调用JavaVMExt对象的LoadNativeLibrary方法:
[java_vm_ext.cc]
这个函数有点长,重点都用注释标注了。
开始的时候会去缓存查看是否已经加载过动态库,如果已经加载过会判断上次加载的ClassLoader和这次加载的ClassLoader是否一致,如果不一致则加载失败,如果一致则返回上次加载的结果,换句话说就是不允许不同的ClassLoader加载同一个动态库。为什么这么做值得推敲。
之后会通过dlopen打开动态共享库。然后会获取动态库中的JNI_OnLoad方法,如果有的话调用之。最后会通过JNI_OnLoad的返回值确定是否加载成功:
这也是为什么在JNI_OnLoad函数中必须正确返回的原因。
可以看到最终没有调用dlclose,当然也不能调用,这里只是加载,真正的函数调用还没有开始,之后就会使用dlopen拿到的句柄来访问动态库中的方法了。
看完这篇文章我们明确了几点:
System.loadLibrary会优先查找apk中的so目录,再查找系统目录,系统目录包括:/vendor/lib(64),/system/lib(64)
不能使用不同的ClassLoader加载同一个动态库
System.loadLibrary加载过程中会调用目标库的JNI_OnLoad方法,我们可以在动态库中加一个JNI_OnLoad方法用于动态注册
如果加了JNI_OnLoad方法,其的返回值为JNI_VERSION_1_2 ,JNI_VERSION_1_4, JNI_VERSION_1_6其一。我们一般使用JNI_VERSION_1_4即可
Android动态库的加载与Linux一致使用dlopen系列函数,通过动态库的句柄和函数名称来调用动态库的函数