本文主要目的是搞清楚so加载和调用的整体脉络,以便遇到so相关的问题时做到心中有数,能迅速把握解决问题的大方向。
先抛出两个问题:
1. 退出插件时如何卸载so,以免内存占用越来越大?
2. 热修复中如何实现native函数的热替换?
这两个问题我会在文章结尾给出解决思路。
so是在System.loadLibrary中加载的,如下:
public static void loadLibrary(String libName) {
Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}
这里传入的ClassLoader正是调用System.loadLibrary的类的ClassLoader,通常为PathClassLoader。
再来看Runtime.loadLibrary的实现:
void loadLibrary(String libraryName, ClassLoader loader) {
if (loader != null) {
String filename = loader.findLibrary(libraryName);
String error = nativeLoad(filename, loader);
return;
}
String filename = System.mapLibraryName(libraryName);
List<String> candidates = new ArrayList<String>();
String lastError = null;
for (String directory : mLibPaths) {
String candidate = directory + filename;
candidates.add(candidate);
if (new File(candidate).exists()) {
String error = nativeLoad(candidate, loader);
if (error == null) {
return;
}
lastError = error;
}
}
..........
}
这里主要做了两件事,首先去查找所有可能的路径判断so是否存在,如果存在就调用nativeLoad加载so。我们重点看so的加载:
static void Dalvik_java_lang_Runtime_nativeLoad(const u4* args,
JValue* pResult)
{
..........
success = dvmLoadNativeCode(fileName, classLoader, &reason);
..........
}
看来真正的实现在dvmLoadNativeCode中,如下:
bool dvmLoadNativeCode(const char* pathName, Object* classLoader,
char** detail)
{
SharedLib* pEntry;
void* handle;
*detail = NULL;
pEntry = findSharedLibEntry(pathName);
if (pEntry != NULL) {
return true;
}
.........
handle = dlopen(pathName, RTLD_LAZY);
SharedLib* pNewEntry;
pNewEntry = (SharedLib*) calloc(1, sizeof(SharedLib));
pNewEntry->pathName = strdup(pathName);
pNewEntry->handle = handle;
pNewEntry->classLoader = classLoader;
pNewEntry->onLoadThreadId = self->threadId;
SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);
bool result = true;
void* vonLoad;
int version;
vonLoad = dlsym(handle, "JNI_OnLoad");
if (vonLoad != NULL) {
OnLoadFunc func = vonLoad;
..........
version = (*func)(gDvm.vmList, NULL);
..........
}
..........
return result;
}
这里主要做了两件事,首先findSharedLibEntry查找so的entry,如果找到则直接返回,否则调用dlopen打开so文件,新建so的entry,并调用addSharedLibEntry将entry添加到系统中,接着调用so中的JNI_OnLoad函数进行初始化。
static SharedLib* findSharedLibEntry(const char* pathName) {
u4 hash = dvmComputeUtf8Hash(pathName);
void* ent;
ent = dvmHashTableLookup(gDvm.nativeLibs, hash, (void*)pathName,
hashcmpNameStr, false);
return ent;
}
可见,已加载的so的entry都会保存在gDvm.nativeLibs中,这是个Hashtable。
接下来我们看看是如何调用so里的函数的。在C中,通常是通过dlsym拿到so中的函数指针,然后再调用,Java中我们貌似没有操心这些,虚拟机一定帮我们都做好了,不过为了搞清原理,我们还是研究一下。
从so中一次性取出各函数的指针然后保存起来,这样下次调用的时候就不用再dlsym了,这个过程就是注册。而注册分两种,主动注册和被动注册。通常在JNI_OnLoad中调用RegisterNatives来主动注册,如下:
static jint RegisterNatives(JNIEnv* env, jclass jclazz,
const JNINativeMethod* methods, jint nMethods)
{
ClassObject* clazz = (ClassObject*) dvmDecodeIndirectRef(env, jclazz);
jint retval = JNI_OK;
int i;
for (i = 0; i < nMethods; i++) {
if (!dvmRegisterJNIMethod(clazz, methods[i].name,
methods[i].signature, methods[i].fnPtr)) {
retval = JNI_ERR;
}
}
return retval;
}
这里会遍历函数指针数组,依次注册每个函数。
static bool dvmRegisterJNIMethod(ClassObject* clazz, const char* methodName,
const char* signature, void* fnPtr) { Method* method = dvmFindDirectMethodByDescriptor(clazz, methodName, signature); if (method == NULL) { method = dvmFindVirtualMethodByDescriptor(clazz, methodName, signature); }
dvmUseJNIBridge(method, fnPtr);
}
这里通过函数名称和签名在指定的类中找到对应的Method,然后调用dvmUseJNIBridge为这个Method设置一个Bridge。
void dvmUseJNIBridge(Method* method, void* func) { DalvikBridgeFunc bridge = shouldTrace(method) ? dvmTraceCallJNIMethod : dvmSelectJNIBridge(method); dvmSetNativeFunc(method, bridge, func); }
这里先获取一个DalvikBridgeFunc,然后再去调dvmSetNativeFunc。
void dvmSetNativeFunc(Method* method, DalvikBridgeFunc func, const u2* insns) { ClassObject* clazz = method->clazz; if (insns != NULL) { method->insns = insns; android_atomic_release_store((int32_t) func, (void*) &method->nativeFunc); } else { method->nativeFunc = func; } }
可见,真正的函数指针赋给了insns,而nativeFunc设成了Bridge,为什么要这么做呢?解释器中执行到一个native函数时,调用的是其nativeFunc,也就是Bridge了,然后再调insns执行真正的函数。
再来看被动注册。在类加载过程中,会扫描类的各个函数,调用loadMethodFromDex加载函数。当发现是native时,会将其nativeFunc设为dvmResolveNativeMethod。当没有主动注册时,调到这个native函数就会执行到dvmResolveNativeMethod,否则就会执行到Bridge。
static void loadMethodFromDex(ClassObject* clazz, const DexMethod* pDexMethod,
Method* meth) { .......... if (dvmIsNativeMethod(meth)) { meth->nativeFunc = dvmResolveNativeMethod; meth->jniArgInfo = computeJniArgInfo(&meth->prototype); } .......... }
再来看看这个dvmResolveNativeMethod:
void dvmResolveNativeMethod(const u4* args, JValue* pResult,
const Method* method, Thread* self)
{
ClassObject* clazz = method->clazz;
void* func;
..........
func = lookupSharedLibMethod(method);
if (func != NULL) {
dvmUseJNIBridge((Method*) method, func);
(*method->nativeFunc)(args, pResult, method, self);
return;
}
}
可见先找到Method对应的函数指针,然后给Method设置一个Bridge,然后执行这个Bridge。看来不管是主动注册还是被动注册,都逃不掉Bridge。我们来看看是怎么通过Method找到对应的函数指针的。
static void* lookupSharedLibMethod(const Method* method) {
return (void*) dvmHashForeach(gDvm.nativeLibs, findMethodInLib,
(void*) method);
}
static int findMethodInLib(void* vlib, void* vmethod) {
..........
preMangleCM =
createJniNameString(meth->clazz->descriptor, meth->name, &len);
mangleCM = mangleString(preMangleCM, len);
func = dlsym(pLib->handle, mangleCM);
..........
return (int) func;
}
dvmHashForeach就是遍历的,参数中传入了一个函数指针,就是说遍历时调这个函数指针来判断是否找到。找到的依据就是通过dlsym找到了我们要的符号。
到这里so的加载就走通了,总结一下,Dalvik虚拟机围绕so做的事总的来说就是dlopen,dlsym做的封装。加载so用dlopen,然后全局缓存起来。当要调so中的函数时,就用dlsym到so中找到对应的函数指针。值得注意的是,类初始化时,所有的native函数的指针都指向了dvmResolveNativeMethod,表示这个函数还有待解析。如果主动在JNI_Onload中注册了,就会将函数指针指向一个Bridge,而真正要执行的函数指针赋给了insns。如果没有主动注册过,执行native函数时就会先解析函数,找到该函数真正要执行的函数指针,然后同样要指定一个Bridge,之后的流程都一样了。
这个Bridge的作用是什么呢?为什么要中间多这么一层呢?一方面是准备参数,另一方面虚拟机调用so中的函数,而so是编译于不同平台下的,不同平台下函数调用的参数传递规则是不同的,是入栈还是放在寄存器中,如果是入栈那么内存布局是什么样的,还有函数返回值怎么传递给调用者,这些都是平台相关的,有的甚至是用汇编写的。所以为了屏蔽这些不同,Dalvik虚拟机用了libffi开源库来统一调用接口,调用时也不是直接调用的,而是统一通过dvmPlatformInvoke来调用so中的函数。感兴趣的可以参考http://sourceware.org/libffi/
回到文章开头提的两个问题:
退出插件时如何卸载so,以免内存占用越来越大?
so卸载关键是拿到dlopen时返回的句柄,而这个句柄是放在so的entry中的,这个entry保存到了gDvm的一个hashtable中。所以卸载so是可能的,虽然系统没有提供现成的接口,不过我们可以hack一下。
热修复中如何实现native函数的热替换?
核心是RegisterNatives,这个函数可以多次注册,自然也就可以替换掉现有的Method中的insns了。