Dalvik虚拟机中so的加载和JNI调用

本文主要目的是搞清楚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/

回到文章开头提的两个问题:

  1. 退出插件时如何卸载so,以免内存占用越来越大?
    so卸载关键是拿到dlopen时返回的句柄,而这个句柄是放在so的entry中的,这个entry保存到了gDvm的一个hashtable中。所以卸载so是可能的,虽然系统没有提供现成的接口,不过我们可以hack一下。

  2. 热修复中如何实现native函数的热替换?
    核心是RegisterNatives,这个函数可以多次注册,自然也就可以替换掉现有的Method中的insns了。

你可能感兴趣的:(Dalvik虚拟机中so的加载和JNI调用)