Android虽然基于Linux系统,但Android本身做了大量的修改。其中对于系统C库更是自己重新实现——Bionic库。在源码目录结构中bionic可以找到相关内容。话题好像偏离有点远,但System.loadLibrary最终还是调用到bionic库中的函数dlopen。这个dlopen搜索依赖so的路径与Linux本身有着不一样的实现,并且在4.2.2版本及以下有着无法隐式加载app目录下依赖so的缺陷,直到4.3才被修复。
关于System.loadLibrary和System.load两个在java中加载so的方法,已经有人进行个大致的分析(Java中System.loadLibrary() 的执行过程 文章中的分析内容是4.2.2版本,仍然不支持隐式加载app目录下依赖so),可以先看看这篇文章里关于以上两个java函数的实现,了解大部分的流程。然后,这里就要开始从dlopen开始分析,对于依赖so加载的问题。
具体分析
首先来看这样一个例子,有两个so,liba.so、libb.so。其中,liba.so引用了libb.so导出的符号,并且是直接引用符号,也就是
需要加载器在运行时的动态隐式链接。在加载liba.so的时候,Java层里,我们会调用System.loadLibrary(),或者System.load()来
指定so的路径,然后最终会调用到native代码中,使用dlopen加载liba.so,就会根据liba.so的Dynamic section中列出类型为
DT_NEEDED的依赖共享库so,并且会递归地一个一个地加载依赖so。在LInux下,加载这些依赖so的路径会是 1. 环境变量LD_LIBRARY指明的路径。2. /etc/ld.so.cache中的函数库列表;3/lib目录,然后/usr/lib。但在4.2.2版本的Android的Bionic实现中,加载的目录只有LD_LIBRARY_PATH变量所指示的路径以及被hardcode进代码的/system/lib,/vendor/lib这两个路径。而事实上LD_LIBRARY_PATH是不建议修改的,而且默认值在Android中也是/system/lib,/vendor/lib这两个路径,因此事实上,被依赖的so加载路径只有/system/lib,/vendor/lib这两个!
不过这种问题在4.3开始得到改正。在4.3版本的代码里,nativeLoad这个方法接受多一个参数ldLibraryPath,这是通过PathClassLoader获取App的native lib路径,然后传给nativeLoad的。nativeLoad在获取这个参数之后,就会调用bionic的方法来显式更新内部的ldPath路径,这样每次加载的时候,就可以先搜索本地App的native lib路径,就能够正确加载依赖的so。
首先我们来看看4.3版本的实现:
private String doLoad(String name, ClassLoader loader) { // Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH, // which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH. // The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load // libraries with no dependencies just fine, but an app that has multiple libraries that // depend on each other needed to load them in most-dependent-first order. // We added API to Android's dynamic linker so we can update the library path used for // the currently-running process. We pull the desired path out of the ClassLoader here // and pass it to nativeLoad so that it can call the private dynamic linker API. // We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the // beginning because multiple apks can run in the same process and third party code can // use its own BaseDexClassLoader. // We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any // dlopen(3) calls made from a .so's JNI_OnLoad to work too. // So, find out what the native library search path is for the ClassLoader in question... String ldLibraryPath = null; if (loader != null && loader instanceof BaseDexClassLoader) { ldLibraryPath = ((BaseDexClassLoader) loader).getLdLibraryPath(); } // nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless // of how many ClassLoaders are in the system, but dalvik doesn't support synchronized // internal natives. synchronized (this) { return nativeLoad(name, loader, ldLibraryPath); } }doLoad方法在System.loadLibrary里被调用,里面的英文注释可以看到这次4.3的改进机制以及原因,主要是通过classloader把native lib的路径传入到bionic中,然后每次加载依赖so都会先从这些路径开始搜索。另外,PathClassLoader的构造是在加载apk的时候,通过传递nativeLibraryDir来初始化的。
static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args, JValue* pResult) { //...... StringObject* ldLibraryPathObj = (StringObject*) args[2]; if (ldLibraryPathObj != NULL) { char* ldLibraryPath = dvmCreateCstrFromString(ldLibraryPathObj); void* sym = dlsym(RTLD_DEFAULT, "android_update_LD_LIBRARY_PATH"); if (sym != NULL) { typedef void (*Fn)(const char*); Fn android_update_LD_LIBRARY_PATH = reinterpret_cast<Fn>(sym); (*android_update_LD_LIBRARY_PATH)(ldLibraryPath); } else { ALOGE("android_update_LD_LIBRARY_PATH not found; .so dependencies will not work!"); } free(ldLibraryPath); } //...... }
可以看到nativeLoad方法会通过显式获取bionic库中的android_update_LD_LIBRARY_PATH方法,然后这个路径最终会更新bionic库的全局变量gLdPaths,这个变量在加载依赖so的时候会产生作用。
static int open_library_on_path(const char* name, const char* const paths[]) { char buf[512]; for (size_t i = 0; paths[i] != NULL; ++i) { int n = __libc_format_buffer(buf, sizeof(buf), "%s/%s", paths[i], name); if (n < 0 || n >= static_cast<int>(sizeof(buf))) { PRINT("Warning: ignoring very long library path: %s/%s", paths[i], name); continue; } int fd = TEMP_FAILURE_RETRY(open(buf, O_RDONLY | O_CLOEXEC)); if (fd != -1) { return fd; } } return -1; } static int open_library(const char* name) { TRACE("[ opening %s ]", name); // If the name contains a slash, we should attempt to open it directly and not search the paths. if (strchr(name, '/') != NULL) { int fd = TEMP_FAILURE_RETRY(open(name, O_RDONLY | O_CLOEXEC)); if (fd != -1) { return fd; } // ...but nvidia binary blobs (at least) rely on this behavior, so fall through for now. } // Otherwise we try LD_LIBRARY_PATH first, and fall back to the built-in well known paths. int fd = open_library_on_path(name, gLdPaths); if (fd == -1) { fd = open_library_on_path(name, gSoPaths); } return fd; }可以看到,open_library会调用gLdPaths里面的路径来加载依赖的so。另外gSoPaths这个全局变量被初始化为/system/lib,/vendor/lib这两个系统路径。
在4.3以下的版本里,gLdPaths的初始化是在linker的初始化里通过读取LD_LIBRARY_PATH来决定的,并不是像4.3这样可以每次动态更新。具体实现如下:
/* skip past the environment */ while(vecs[0] != 0) { if(!strncmp((char*) vecs[0], "DEBUG=", 6)) { debug_verbosity = atoi(((char*) vecs[0]) + 6); } else if(!strncmp((char*) vecs[0], "LD_LIBRARY_PATH=", 16)) { ldpath_env = (char*) vecs[0] + 16; } vecs++; } //...... /* Use LD_LIBRARY_PATH if we aren't setuid/setgid */ if (ldpath_env && getuid() == geteuid() && getgid() == getegid()) parse_library_path(ldpath_env, ":");
以上这段代码是在__linker_init函数里的代码片段,parse_library_path函数的作用就是设置加载依赖so的路径。另外,关于至于如何解析so里的dynamic section以及如何加载,并且初始化so等这里不属于本次讨论范畴,就不再赘述了。
解决办法:
由于加载依赖so的路径在4.3版本以前会存在问题(即调用System.loadLibrary("a");会失败),所以我们只能够在显式加载每个依赖最低的库,这样最后加载依赖最高的so就会成功。根据上面的例子,我们可以先显式加载libb.so,然后再加载liba.so。代码如下:
System.loadLibrary("b"); System.loadLibrary("a");这样就可以成功加载两个so。