深入理解 System.loadLibrary

 转载:深入理解 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系列函数,通过动态库的句柄和函数名称来调用动态库的函数

你可能感兴趣的:(深入理解 System.loadLibrary)