本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/57bec216d81f2415515d3e9c
作者:陈昱全
引言
随着项目中动态链接库越来越多,我们也遇到了很多奇怪的问题,比如只在某一种 OS 上会出现的 java.lang.UnsatisfiedLinkError
,但是明明我们动态库名称没错,ABI 也没错,方法也能对应的上,而且还只出现在某一些机型上,搞的我们百思不得其解。为了找到出现千奇百怪问题的原因,和能够提供一个方式来解决一些比较奇怪的动态库加载的问题,我发现了解一下 so 的加载流程是非常有必要的了,便于我们发现问题和解决问题,这就是本文的由来。
要想了解动态链接库是如何加载的,首先是查看动态链接库是怎么加载的,从我们日常调用的 System.loadLibrary
开始。
为了书写方便,后文中会用“so”来简单替代“动态链接库”概念。
1、动态链接库的加载流程
首先从宏观流程上来看,对于 load 过程我们分为 find&load,首先是要找到 so 所在的位置,然后才是 load 加载进内存,同时对于 dalvik 和 art 虚拟机来说,他们加载 so 的流程和方式也不尽相同,考虑到历史的进程我们分析 art 虚拟机的加载方式,先贴一张图看看 so 加载的大概流程。
我的疑问
- ClassLoader 是如何去找到so的呢?
- 如何判断这个 so 是否加载过?
- native 库的地址是如何来的
- so 是怎么弄到 native 库里面去的?
- 如何决定 app 进程是32位还是64位的?
找到以上的几个问题的答案,可以帮我们了解到哪个步骤没有找到动态链接库,是因为名字不对,还是 app 安装后没有拷贝过来动态链接库还是其他原因等,我们先从第一个问题来了解。
2、ClassLoader 如何找 so 呢?
首先我们从调用源码看起,了解 System.loadLibrary
是如何去找到 so 的。
System.java
public void loadLibrary(String nickname) {
loadLibrary(nickname, VMStack.getCallingClassLoader());
}
通过 ClassLoader 的 findLibaray 来找到 so 的地址
void loadLibrary(String libraryName, ClassLoader loader) {
if (loader != null) {
String filename = loader.findLibrary(libraryName);
if (filename == null) {
// It's not necessarily true that the ClassLoader used
// System.mapLibraryName, but the default setup does, and it's
// misleading to say we didn't find "libMyLibrary.so" when we
// actually searched for "liblibMyLibrary.so.so".
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
String error = doLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
return;
}
如果这里没有找到就要抛出来 so 没有找到的错误了,这个也是我们非常常见的错误。所以这里我们很需要知道这个 ClassLoader 是哪里来的。
2.1 ClassLoader 怎么来的?
这里的一切都要比较熟悉 app 的启动流程,关于 app 启动的流程网上已经说过很多了,我就不再详细说了,一个 app 的启动入口是在 ActivityThread 的 main 函数里,这里启动了我们的 UI 线程,最终启动流程会走到我们在 ActivityThread 的 handleBindApplication 函数中。
private void handleBindApplication(AppBindData data) {
......
......
ContextImpl instrContext = ContextImpl.createAppContext(this, pi);
try {
java.lang.ClassLoader cl = instrContext.getClassLoader();
mInstrumentation = (Instrumentation)
cl.loadClass(data.instrumentationName.getClassName()).newInstance();
} catch (Exception e) {
throw new RuntimeException(
"Unable to instantiate instrumentation "
+ data.instrumentationName + ": " + e.toString(), e);
}
mInstrumentation.init(this, instrContext, appContext,
new ComponentName(ii.packageName, ii.name), data.instrumentationWatcher,
data.instrumentationUiAutomationConnection);
......
......
} finally {
StrictMode.setThreadPolicy(savedPolicy);
}
}
我们找到了这个 classLoader 是从 ContextImpl 中拿过来的,有兴趣的同学可以一步步看看代码,最后的初始化其实是在 ApplicationLoaders 的 getClassLoader 中
ApplicationLoaders.java
public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)
{
......
......
if (parent == baseParent) {
ClassLoader loader = mLoaders.get(zip);
if (loader != null) {
return loader;
}
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
PathClassLoader pathClassloader =
new PathClassLoader(zip, libPath, parent);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
mLoaders.put(zip, pathClassloader);
return pathClassloader;
}
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
PathClassLoader pathClassloader = new PathClassLoader(zip, parent);
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
return pathClassloader;
}
}
其实是一个 PathClassLoader,他的基类是 BaseDexClassLoader,在他其中的实现了我们上文看到的 findLibrary 这个函数,通过 DexPathList 去 findLibrary。
BaseDexClassLoader.java
public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
for (File directory : nativeLibraryDirectories) {
File file = new File(directory, fileName);
if (file.exists() && file.isFile() && file.canRead()) {
return file.getPath();
}
}
return null;
}
代码的意思很简单,其实就是首先给 so 拼成完整的名字比如a拼接成 liba.so 这样,然后再从存放 so 的文件夹中找这个 so,在哪个文件夹里面找到了,我们就返回他的绝对路径。所以这里最关键的就是如何知道这个 nativeLibraryDirectories 的值是多少,于是也引出我们下一个疑问, native 地址库是怎么来的,是多少呢?
3 nativeLibraryDirectories 是怎么来的?
通过查看 DexPathList 可以知道,这个 nativeLibraryDirectories 的值来自于2个方面,一个是来自外部传过来的 libraryPath,一个是来自 java.library.path
这个环境变量的值。
DexPathList.java
private static File[] splitLibraryPath(String path) {
/*
* 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
* 2. the VM's libra