美文网首页Android进阶之路Android面试
NDK | 说说 so 库从加载到卸载的全过程

NDK | 说说 so 库从加载到卸载的全过程

作者: 彭旭锐 | 来源:发表于2020-11-09 00:23 被阅读0次

    前言

    • 在 JNI 开发中,必然需要用到 so 库,那么你清楚 so 库从加载到卸载的全过程吗?;
    • 在这篇文章里,我将带你建立对 so 库从加载进内存到卸载整个过程的理解。另外,文末的应试建议也不要错过哦,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。

    相关文章


    目录


    1. 获取 so 库

    关于 获取 so 库的具体步骤,我在这篇文章里讨论,《NDK | 一篇文章开启你的 NDK 技能树》,请关注。通常来说,最终生成的 so 库命名为lib[name].so,例如系统内置的 so 库:


    2. 加载 so 库

    首先,让我们看看加载 so 库的入口,加载动态库需要使用System.load(...)System.loadLibrary(...)。通常来说,都会放在static {}中执行。

    System.java

    public static void load(String filename) {
        1. 委派给 Runtime#load0(...)
        Runtime.getRuntime().load0(VMStack.getStackClass1(), filename);
    }
    
    public static void loadLibrary(String libname) {
        2. 委派给 Runtime#loadLibrary0(...)
        Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
    }
    

    其中,getCallingClassLoader()返回的是加载调用者使用的 ClassLoader。

    2.1 Runtime#load0(...) 源码分析

    Runtime.java

    -> 1(已简化)
    synchronized void load0(Class<?> fromClass, String filename) {
        1.1 检查是否为绝对路径
        if (!(new File(filename).isAbsolute())) {
            throw new UnsatisfiedLinkError("Expecting an absolute path of the library: " + filename);
        }
    
        1.2 调用 nativeLoad(【绝对路径】) 加载动态库
        String error = nativeLoad(filename, fromClass.getClassLoader());
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
    }
    

    可以看到,Runtime#load0(...)的逻辑比较简单:

    • 1.1 确保参数filename是一个绝对路径
    • 1.2 调用nativeLoad(【绝对路径】)加载动态库,这个方法我在 第 3 节 nativeLoad(...) 主流程源码分析 说。

    2.2 Runtime#loadLibrary0(...) 源码分析

    Runtime.java

    -> 2(已简化)
    synchronized void loadLibrary0(ClassLoader loader, String libname) {
        2.1 检查是否出现路径分隔符
        if (libname.indexOf((int)File.separatorChar) != -1) {
            throw new UnsatisfiedLinkError("Directory separator should not appear in library name: " + libname);
        }
    
        String libraryName = libname;
        2.2 ClassLoader 非空
    
        if (loader != null) {
            2.2.1 根据动态库名称查询动态库的绝对路径
            String filename = loader.findLibrary(libraryName);
            if (filename == null) {
                throw new UnsatisfiedLinkError(...);
            }
    
            2.2.2 调用 nativeLoad(【绝对路径】) 加载动态库
            String error = nativeLoad(filename, loader);
            if (error != null) {
                throw new UnsatisfiedLinkError(error);
            }
            return;
        }
        
        2.3 ClassLoader 为空(丑丑也不知道什么场景会为空)
    
        2.3.1 拼接 lib 前缀与.so 后缀
        String filename = System.mapLibraryName(libraryName);
        List<String> candidates = new ArrayList<String>();
    
        2.3.2 遍历每个 so 库存储路径
        String lastError = null;
        for (String directory : getLibPaths()) {
            String candidate = directory + filename;
            candidates.add(candidate);
            2.3.3 调用 nativeLoad(【绝对路径】) 加载动态库
            String error = nativeLoad(candidate, loader);
            if (error == null) {
                return
            }
        }
        throw new UnsatisfiedLinkError(...);
    }
    

    可以看到,Runtime#loadLibrary0(...) 主要分为 ClassLoader 为非空与为空两种情况。

    先看 ClassLoader 非空的情况:

    • 2.2.1 调用ClassLoader#findLibrary(libraryName)查询动态库的绝对路径,这个方法我后文再说。
    • 2.2.2 调用nativeLoad(【绝对路径】)加载动态库

    再看下 ClassLoader 为空的情况(一般不会):

    System.java

    -> 2.3.1
    public static native String mapLibraryName(String libname);
    

    System.c

    JNIEXPORT jstring JNICALL
    System_mapLibraryName(JNIEnv *env, jclass ign, jstring libname) {
        1、libname 拼接 JNI_LIB_PREFIX(lib) 前缀
        2、libname 拼接 JNI_LIB_SUFFIX(.so) 后缀
    }
    

    jvm_md.h

    #define JNI_LIB_PREFIX "lib"
    #define JNI_LIB_SUFFIX ".so"
    

    Runtime.java

    -> 2.3.2(已简化,源码基于 DCL 单例)
    private String[] getLibPaths() {
        String javaLibraryPath = System.getProperty("java.library.path");
        String[] paths = javaLibraryPath.split(":");
        return paths;
    }
    
    • 2.3.1 调用 native 方法System.mapLibraryName(),拼接 lib 前缀与.so 后缀
    • 2.3.2 调用System.getProperty("java.library.path")获取系统 so 库存储路径
    • 2.3.3 遍历每个 so 库存储路径,拼接除动态库的绝对路径,调用nativeLoad(【绝对路径】)加载动态库

    关于 System.getProperty("java.library.path") 的源码分析,在我之前写过的一篇文章里讲过:《NDK | 带你探究 getProperty() 获取系统属性原理》,这里我简单复述一下:

    1、"java.library.path"这个属性是由运行环境管理的;
    2、对于 64 位系统,返回的是"/system/lib64" 、 "/vendor/lib64"
    3、对于 32 位系统,返回的是"/system/lib" 、 "/vendor/lib"

    可以看到,对于 ClassLoader 非空和为空两种情况,其实最后都需要调用nativeLoad(【绝对路径】)加载动态库,这其实和Runtime#load0(...)的逻辑一致。这个方法我在 第 3 节 nativeLoad(...) 主流程源码分析 分析。

    2.3 ClassLoader#findLibrary(libraryName) 源码分析

    对了,在前面讲到 ClassLoader 非空的情况时,ClassLoader#findLibrary(libraryName)还没有分析,现在讲下。在 Android 系统中,ClassLoader 通常是 PathClassLoader:

    PathClassLoader.java

    public class PathClassLoader extends BaseDexClassLoader {
        public PathClassLoader(String dexPath, ClassLoader parent) {
            super(dexPath, null, null, parent);
        }
    
        public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
            super(dexPath, null, librarySearchPath, parent);
        }
    }
    

    BaseDexClassLoader.java

    public class BaseDexClassLoader extends ClassLoader {
        public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent, boolean isTrusted) {
            super(parent);
            this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
        }
    
        public String findLibrary(String name) {
            return pathList.findLibrary(name);
        }
    
        ...
    }
    

    PathClassLoader 没用重写findLibrary(),所以主要的逻辑还是在 BaseDexClassLoader 中,最终是委派给 DexPathList 处理的:

    DexPathList.java

    -> 2.2.1 根据动态库名称查询动态库的绝对路径
    public String findLibrary(String libraryName) {
        1、拼接 lib 前缀与.so 后缀
        String fileName = System.mapLibraryName(libraryName);
        2、遍历 nativeLibraryPathElements 路径
        for (NativeLibraryElement element : nativeLibraryPathElements) {
            3、搜索目标 so 库
            String path = element.findNativeLibrary(fileName);
            if (path != null) {
                return path;
            }
        }
        return null;
    }
    
    NativeLibraryElement[] nativeLibraryPathElements;
    private Element[] dexElements;
    private final List<File> nativeLibraryDirectories;
    private final List<File> systemNativeLibraryDirectories;
    
    public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) {
        this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false);
    }
    
    0、 初始化
    DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
        ...
        所有 Dex 文件
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext, isTrusted);
    
        app 目录的 so 库路径
        this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
    
        系统的 so 库路径("java.library.path"))
        this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
    
        记录 app 和系统的 so 库路径
        List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
        allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
        this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
    
        ...
    }
    

    可以看到,DexPathList#findLibrary(...)主要分为 3 个步骤:

    • 1、拼接 lib 前缀与.so 后缀
    • 2、遍历nativeLibraryPathElements路径
    • 3、搜索目标 so 库,如果存在,返回拼接后的绝对路径

    其中nativeLibraryPathElements路径由两部分组成:

    • 1、app 目录下的 so 库路径(/data/app/[packagename]/lib/arm64
    • 2、系统 so 库存储路径(/system/lib64、/vendor/lib64

    2.4 小结

    最后,总结System.load(...)System.loadLibrary(...)的异同:

    不同点:

    • System.load(...)指定的是 so 库的绝对路径,只会在该路径搜索 so 库;
    • System.loadLibrary(...)指定的是 so 库的名称,查找时会自动拼接 lib 前缀和 .so 后缀,并在 app 路径和系统路径搜索。

    共同点:

    • 两个方法最终都得到一个绝对路径,并调用 native 方法 nativeLoad(【绝对路径】)加载动态库。

    到目前为止,调用栈如下:

    System.loadLibrary(libPath)
    -> Runtime.load0(libPath)
        -> nativeLoad(libPath)
    
    System.loadLibrary(libName)
    -> Runtime.loadLibrary0(libNane)
        -> ClassLoader#findLibrary(libName)-> DexPathList#findLibrary(libName)  
        -> nativeLoad(libPath)
    

    3. nativeLoad(...) 主流程源码分析

    经过前面的分析,取到 so 库的绝对路径之后,最终是调用 native 方法nativeLoad(...)加载 so 库,相关源码如下:

    Runtime.java

    -> 1.2 / 2.2.2 / 2.3.3
    private static native String nativeLoad(String filename, ClassLoader loader);
    

    Runtime.c

    JNIEXPORT jstring JNICALL
    Runtime_nativeLoad(JNIEnv* env, jclass ignored, jstring javaFilename, jobject javaLoader) { 
        return JVM_NativeLoad(env, javaFilename, javaLoader); 
    }
    

    最终调用到:java_vm_ext.cc

    共享库列表
    std::unique_ptr<Libraries> libraries_;
    
    已简化
    bool JavaVMExt::LoadNativeLibrary(JNIEnv* env,
                                      const std::string& path,
                                      jobject class_loader,
                                      std::string* error_msg) {
        SharedLibrary* library;
        Thread* self = Thread::Current();
    
        1、检查是否已经加载过
        library = libraries_->Get(path);
    
        2、已经加载过,跳过
        if (library != nullptr) {
            ...
            return true;
        }
    
        3、调用 dlopen 打开 so 库
        void* handle = dlopen(path,RTLD_NOW);
    
        4、创建共享库
        std::unique_ptr<SharedLibrary> new_library(
            new SharedLibrary(env,
                              self,
                              path,
                              handle,
                              needs_native_bridge,
                              关注点:共享库中持有 ClassLoader(卸载 so 库时用到)
                              class_loader,
                              class_loader_allocator));
    
        5、将共享库记录到 libraries_ 表中
        libraries_->Put(path, library);
    
        6、调用 so 库中的 JNI_OnLoad 方法
        void* sym = dlsym(library,"JNI_OnLoad");
        typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
        JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
        int version = (*jni_on_load)(this, nullptr);
    
        return true
    }
    

    上面的代码已经非常简化了,主要关注以下几点:

    • 1、检查是否已经加载过(libraries_记录了已经加载过的 so 库);
    • 2、如果已经加载过,跳过;
    • 3、调用dlopen打开 so 库;
    • 4、创建共享库SharedLibrary,这个就是 so 库的内存表示,需要注意的是,SharedLibrary 和 ClassLoader 是有关联的(SharedLibrary 持有了 ClassLoader),这一点在卸载 so 库的时候会用到;
    • 5、将共享库记录到libraries_表中;
    • 6、调用 so 库中的JNI_OnLoad方法,返回值是jint类型,告诉虚拟机此 so 库使用的 JNI版本

    整个加载的过程:


    4. 卸载 so 库

    JDK 没有提供直接卸载 so 库的方法,而是 在ClassLoader 卸载时跟随卸载,具体触发的地方在虚拟机堆执行垃圾回收的源码:

    heap.cc

    collector::GcType Heap::CollectGarbageInternal(collector::GcType gc_type,
                                                   GcCause gc_cause,
                                                   bool clear_soft_references) {
        ...
        soa.Vm()->UnloadNativeLibraries();
    }
    

    这里我们只关注与共享库有关的代码,最终调用到:java_vm_ext.cc

    已简化
    void UnloadNativeLibraries(){
        1、遍历共享库列表 libraries_
        for (auto it = libraries_.begin(); it != libraries_.end(); ) {
            SharedLibrary* const library = it->second;
            
            2、检查关联的 ClassLoader 是否卸载(unload)
            const jweak class_loader = library->GetClassLoader();
            if (class_loader != nullptr && self->IsJWeakCleared(class_loader)) {
            
                3、记录需要卸载的共享库
                unload_libraries.push_back(library);
                it = libraries_.erase(it);
            } else {
                ++it;
            }
        }
        4、遍历需要卸载的共享库,执行 JNI_OnUnloadFn()
        typedef void (*JNI_OnUnloadFn)(JavaVM*, void*);
        for (auto library : unload_libraries) {
            void* const sym = dlsym(library, "JNI_OnUnload")
            JNI_OnUnloadFn jni_on_unload = reinterpret_cast<JNI_OnUnloadFn>(sym);
            jni_on_unload(self->GetJniEnv()->GetVm(), nullptr);
            
            5、回收内存
            delete library;
        }
    }
    

    上面的代码已经非常简化了,主要关注以下几点:

    • 1、遍历共享库列表libraries_
    • 2、检查关联的 ClassLoader 是否卸载(unload)
    • 3、记录需要卸载的共享库
    • 4、遍历需要卸载的共享库,执行JNI_OnUnloadFn(),返回值是void
    • 5、回收内存

    5. 总结

    • 应试建议
      1、应知晓 so 库加载到卸载的大体过程,主要分为:确定 so 库绝对路径、nativeLoad 加载进内存、ClassLoader 卸载时跟随卸载
      2、应知晓搜索 so 库的路径,分为 App 路径和系统路径
      3、应知晓JNI_OnLoadJNI_OnUnLoad的执行时机(分别在加载与卸载时执行)

    参考资料

    推荐阅读

    感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的GitHub!

    相关文章

      网友评论

        本文标题:NDK | 说说 so 库从加载到卸载的全过程

        本文链接:https://www.haomeiwen.com/subject/htqtbktx.html