在实现android插件化过程中,在插件代码中加载so时出现了一些问题,因此特地研究了一下android系统中加载so的过程,记录下来,整理成文。
在android系统中,加载so一般会调用System.loadLibrary(name)或者是System.load(path),这两个函数都可以用来加载so文件,区别在于System.loadLibrary函数的参数为库文件名,而System.load函数的 参数为库文件的绝对路径,可以是任意路径(路径需要可执行权限)。这两个函数本质上都是一样的,只是搜索so的搜索目录略有差别。下面以System.loadLibrary函数为例来分析加载so的实现原理。
首先看一下System.loadLibrary函数的源码(ibcore/luni/src/main/java/java/lang/System.java):
代码语言:javascript复制 public static void loadLibrary(String libName) {
SecurityManager smngr = System.getSecurityManager();
if (smngr != null) {
smngr.checkLink(libName);
}
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}
如上所示,通过VMStack拿到了当前的ClassLoader,然后将加载so的动作委托给Runtime.loadLibrary去执行(libcore/luni/src/main/java/java/lang/Runtime.java):
代码语言:javascript复制 void loadLibrary(String libname, ClassLoader loader) {
String filename;
int i;
if (loader != null) {
filename = loader.findLibrary(libname);
if (filename != null && nativeLoad(filename, loader))
return;
// else fall through to exception
} else {
filename = System.mapLibraryName(libname);
for (i = 0; i < mLibPaths.length; i ) {
if (false)
System.out.println("Trying " mLibPaths[i] filename);
if (nativeLoad(mLibPaths[i] filename, loader))
return;
}
}
throw new UnsatisfiedLinkError("Library " libname " not found");
如上图所示,正常情况下Classloader不会为空,因此进入第一个if分支,可以看看出在此函数中完成了两件事:
1:通过调用ClassLoader.findLibrary函数去拿到so的真正的文件路径;
2:调用nativeLoad函数去实现真正的so加载;
这里会牵扯到一个问题,如何通过so的名称去ClassLoader拿到so真正的文件路径?Android系统中可供使用的ClassLoader有两个,分别是DexClassLoader和PathClassLoader,其中PathClassLoader一般用于加载已经安装过的系统app的dex文件,而DexClassLoader可以加载任意路径的apk/jar文件(此文件路径需要可执行权限),两者间的具体差别请参考developer.android.com。DexClassLoader和PathClassLoader对于findLibrary函数的实现大致相同,下面来看看PathClassLoader中findLibrary函数的实现(libcore/luni/src/main/java/java/PathClassLoader.java):
代码语言:javascript复制protected String findLibrary(String libname) {
ensureInit();
String fileName = System.mapLibraryName(libname);
for (int i = 0; i < mLibPaths.length; i ) {
String pathName = mLibPaths[i] fileName;
File test = new File(pathName);
if (test.exists())
return pathName;
}
return null;
}
如上所示,首先调用System.mapLibraryName拿到so的前缀和后缀名,如libname为hello,那么经过此函数转换后变成了libhello.so,然后在mLibPaths搜索目录下搜寻libhello.so文件,如果文件存在,则代表找到了此so的文件,直接返回即可。mLibPaths是如何初始化的呢?还有如上所示,在mLibPaths搜索目录下搜寻是有序的,只要搜索到了就立即返回,因此如果在mLibPaths[0]和mLibPaths[1]目录下均有这个so,会优先返回mLibPaths[0]目录下so的文件路径的,因此我们也需要关注mLibPaths中搜索目录的顺序。
在findLibrary函数开始调用ensureInit函数后会初始化mLibPaths搜索目录,下面看看这个函数的具体实现(主要关注mLibPaths中搜索目录的内容和顺序,以下代码省略了部分无关代码)。
代码语言:javascript复制 private synchronized void ensureInit() {
if (initialized) {
return;
}
initialized = true;
/*
* Prep for native library loading.
*/
String pathList = System.getProperty("java.library.path", ".");
String pathSep = System.getProperty("path.separator", ":");
String fileSep = System.getProperty("file.separator", "/");
if (libPath != null) {
if (pathList.length() > 0) {
pathList = pathSep libPath;
}
else {
pathList = libPath;
}
}
mLibPaths = pathList.split(pathSep);
length = mLibPaths.length;
// Add a '/' to the end so we don't have to do the property lookup
// and concatenation later.
for (int i = 0; i < length; i ) {
if (!mLibPaths[i].endsWith(fileSep))
mLibPaths[i] = fileSep;
if (false)
System.out.println("Native lib path: " mLibPaths[i]);
}
}
这段代码看上去挺简单,主要是从系统获取到"java.library.path"属性,libPath为应用程序的搜索目录,libPath是在构造PathClassLoader时由系统传进来的(一般不会为空),如果libPath不为空,则添加到mLibPaths,由代码可以确定搜索目录的顺序是系统的搜索目录优先,应用程序的搜索目录在最后。
总结一下,ClassLoader的findLibrary实际上会去两部分目录下搜索so,一部分是通过System.getProperty("java.library.path", ".")拿到的系统搜索目录,还有部分是在构造PathClassLoader时传进来的librarypath。在三星手机上,mLibPaths分别如下:
1:/vendor/lib
2:/system/lib
3:/data/data/应用包名/lib
每个手机可能根据系统的不同而有不同的应用程序搜索目录,如在有些手机上应用程序的搜索目录为/data/app-lib/.apk名称 目录下(可以参考PackageManagerService中部分代码);
解决了ClassLoader.findLibrary函数的问题,现在去看看nativeLoad函数的实现。nativeLoad是native函数,真正的实现位于/android_dalvik-eclair/vm//native/java_lang_Runtime.c:
代码语言:javascript复制static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args,
JValue* pResult)
{
StringObject* fileNameObj = (StringObject*) args[0];
Object* classLoader = (Object*) args[1];
char* fileName;
int result;
if (fileNameObj == NULL)
RETURN_INT(false);
fileName = dvmCreateCstrFromString(fileNameObj);
result = dvmLoadNativeCode(fileName, classLoader);
free(fileName);
RETURN_INT(result);
}
可以看到,nativeLoad()实际上只是完成了两件事情,第一,是调用dvmCreateCstrFromString()将Java 的library path String 转换到native的String,然后将这个path传给dvmLoadNativeCode()做load,dvmLoadNativeCode()这个函数的实现在/android_dalvik-eclair/vm/native.c中,如下:
代码语言:javascript复制bool dvmLoadNativeCode(const char* pathName, Object* classLoader)
{
SharedLib* pEntry;
void* handle;
LOGD("Trying to load lib %s %pn", pathName, classLoader);
/*
* See if we've already loaded it. If we have, and the class loader
* matches, return successfully without doing anything.
*/
pEntry = findSharedLibEntry(pathName);
if (pEntry != NULL) {
if (pEntry->classLoader != classLoader) {
LOGW("Shared lib '%s' already opened by CL %p; can't open in %pn",
pathName, pEntry->classLoader, classLoader);
return false;
}
LOGD("Shared lib '%s' already loaded in same CL %pn",
pathName, classLoader);
if (!checkOnLoadResult(pEntry))
return false;
return true;
}
Thread* self = dvmThreadSelf();
int oldStatus = dvmChangeStatus(self, THREAD_VMWAIT);
handle = dlopen(pathName, RTLD_LAZY);
dvmChangeStatus(self, oldStatus);
if (handle == NULL) {
LOGI("Unable to dlopen(%s): %sn", pathName, dlerror());
return false;
}
/* create a new entry */
SharedLib* pNewEntry;
pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib));
pNewEntry->pathName = strdup(pathName);
pNewEntry->handle = handle;
pNewEntry->classLoader = classLoader;
dvmInitMutex(&pNewEntry->onLoadLock);
pthread_cond_init(&pNewEntry->onLoadCond, NULL);
pNewEntry->onLoadThreadId = self->threadId;
/* try to add it to the list */
SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);
if (pNewEntry != pActualEntry) {
LOGI("WOW: we lost a race to add a shared lib (%s CL=%p)n",
pathName, classLoader);
freeSharedLibEntry(pNewEntry);
return checkOnLoadResult(pActualEntry);
} else {
LOGD("Added shared lib %s %pn", pathName, classLoader);
bool result = true;
void* vonLoad;
int version;
vonLoad = dlsym(handle, "JNI_OnLoad");
if (vonLoad == NULL) {
LOGD("No JNI_OnLoad found in %s %pn", pathName, classLoader);
} else {
/*
* Call JNI_OnLoad. We have to override the current class
* loader, which will always be "null" since the stuff at the
* top of the stack is around Runtime.loadLibrary(). (See
* the comments in the JNI FindClass function.)
*/
OnLoadFunc func = vonLoad;
Object* prevOverride = self->classLoaderOverride;
self->classLoaderOverride = classLoader;
oldStatus = dvmChangeStatus(self, THREAD_NATIVE);
LOGV(" calling JNI_OnLoad(%s)n", pathName);
version = (*func)(gDvm.vmList, NULL);
dvmChangeStatus(self, oldStatus);
self->classLoaderOverride = prevOverride;
if (version != JNI_VERSION_1_2 && version != JNI_VERSION_1_4 &&
version != JNI_VERSION_1_6)
{
LOGW("JNI_OnLoad returned bad version (%d) in %s %pn",
version, pathName, classLoader);
/*
* It's unwise to call dlclose() here, but we can mark it
* as bad and ensure that future load attempts will fail.
*
* We don't know how far JNI_OnLoad got, so there could
* be some partially-initialized stuff accessible through
* newly-registered native method calls. We could try to
* unregister them, but that doesn't seem worthwhile.
*/
result = false;
} else {
LOGV(" finished JNI_OnLoad %sn", pathName);
}
}
if (result)
pNewEntry->onLoadResult = kOnLoadOkay;
else
pNewEntry->onLoadResult = kOnLoadFailed;
pNewEntry->onLoadThreadId = 0;
/*
* Broadcast a wakeup to anybody sleeping on the condition variable. */
dvmLockMutex(&pNewEntry->onLoadLock);
pthread_cond_broadcast(&pNewEntry->onLoadCond);
dvmUnlockMutex(&pNewEntry->onLoadLock);
return result;
}
}
dvmLoadNativeCode()首先会检测是否已经加载过这个so(findSharedLibEntry),如果已经加载过了,那么直接返回即可;如果没有加载,那么重新加载一遍,加载的过程可以用下面的流程来描述:
- 调用dlopen() 打开一个so文件,取得该so的文件句柄;
- 调用dlsym()函数,查找到so文件中的JNI_OnLoad()这个函数的函数指针;
- 执行JNI_OnLoad()函数;