前言
- 在 JNI 开发中,必然需要用到 so 库,那么你清楚 so 库从加载到卸载的全过程吗?
- 在这篇文章里,我将带你建立对 so 库从加载进内存到卸载整个过程的理解。另外,文末的应试建议也不要错过哦,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
相关文章
- 《NDK | 说说 so 库从加载到卸载的全过程》
- 《NDK | 带你梳理 JNI 函数注册的方式和时机》
- 《NDK | 带你探究 getProperty() 获取系统属性原理》
- 《NDK | 一篇文章带你点亮 JNI 开发基石符文》(快写好了)
- 《NDK | 一篇文章开启你的 NDK 技能树》(真的快写好了)
目录
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); 复制代码
JNIEXPORT jstring JNICALL System_mapLibraryName(JNIEnv *env, jclass ign, jstring libname) { 1、libname 拼接 JNI_LIB_PREFIX(lib) 前缀 2、libname 拼接 JNI_LIB_SUFFIX(.so) 后缀 } 复制代码
#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:
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); } } 复制代码
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 处理的:
-> 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)
Native libraries may exist in both the system and application library paths, and we use this search order:
- This class loader's library path for application librarie (librarySearchPath):
1.1. Native library directories
1.2. Path to libraries in apk-files
- The VM's library path from the system property for system libraries also known as java.library.path
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); 复制代码
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 卸载时跟随卸载,具体触发的地方在虚拟机堆执行垃圾回收的源码:
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_OnUnload(),返回值是void - 5、回收内存
5. 总结
- 应试建议 1、应知晓 so 库加载到卸载的大体过程,主要分为:确定 so 库绝对路径、nativeLoad 加载进内存、ClassLoader 卸载时跟随卸载;
2、应知晓搜索 so 库的路径,分为 App 路径和系统路径
3、应知晓JNI_OnLoad 与JNI_OnUnLoad 的执行时机(分别在加载与卸载时执行)





