「NDK 路线」| so 库加载到卸载的全过程

前言

  • 在 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的执行时机(分别在加载与卸载时执行)

参考资料

创作不易,你的「三连」是丑丑最大的动力,我们下次见!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343