点赞关注,不再迷路,你的支持对我意义重大!
🔥 Hi,我是丑丑。本文 GitHub · Android-NoteBook 已收录,这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)
前言
- 在 JNI 开发中,必然需要用到 so 库,那么你清楚 so 库从加载到卸载的全过程吗?
- 在这篇文章里,我将带你建立对 so 库从加载进内存到卸载整个过程的理解。另外,文末的应试建议也不要错过哦,如果能帮上忙,请务必点赞加关注,这真的对我非常重要。
相关文章
- 《NDK | 说说 so 库从加载到卸载的全过程》
- 《NDK | 带你梳理 JNI 函数注册的方式和时机》
- 《NDK | 带你探究 getProperty() 获取系统属性原理》
- 《NDK | 一篇文章带你点亮 JNI 开发基石符文》(快写好了)
- 《NDK | 一篇文章开启你的 NDK 技能树》(真的快写好了)
目录
- 获取 so 库
关于 获取 so 库的具体步骤,我在这篇文章里讨论,《NDK | 一篇文章开启你的 NDK 技能树》,请关注。通常来说,最终生成的 so 库命名为lib[name].so
,例如系统内置的 so 库:
- 加载 so 库
首先,让我们看看加载 so 库的入口,加载动态库需要使用System.load(...)
或 System.loadLibrary(...)
。通常来说,都会放在static {}
中执行。
System.java
1 | scss复制代码public static void load(String filename) { |
其中,getCallingClassLoader()
返回的是加载调用者使用的 ClassLoader。
2.1 Runtime#load0(…) 源码分析
Runtime.java
1 | scss复制代码-> 1(已简化) |
可以看到,Runtime#load0(...)
的逻辑比较简单:
- 1.1 确保参数
filename
是一个绝对路径 - 1.2 调用
nativeLoad(【绝对路径】)
加载动态库,这个方法我在 第 3 节 nativeLoad(…) 主流程源码分析 说。
2.2 Runtime#loadLibrary0(…) 源码分析
Runtime.java
1 | ini复制代码-> 2(已简化) |
可以看到,Runtime#loadLibrary0(...)
主要分为 ClassLoader 为非空与为空两种情况。
先看 ClassLoader 非空的情况:
- 2.2.1 调用
ClassLoader#findLibrary(libraryName)
查询动态库的绝对路径,这个方法我后文再说。 - 2.2.2 调用
nativeLoad(【绝对路径】)
加载动态库
再看下 ClassLoader 为空的情况(一般不会):
System.java
1 | arduino复制代码-> 2.3.1 |
1 | kotlin复制代码JNIEXPORT jstring JNICALL |
1 | arduino复制代码#define JNI_LIB_PREFIX "lib" |
Runtime.java
1 | ini复制代码-> 2.3.2(已简化,源码基于 DCL 单例) |
- 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:
1 | scala复制代码public class PathClassLoader extends BaseDexClassLoader { |
1 | scala复制代码public class BaseDexClassLoader extends ClassLoader { |
PathClassLoader 没用重写findLibrary()
,所以主要的逻辑还是在 BaseDexClassLoader 中,最终是委派给 DexPathList 处理的:
1 | arduino复制代码-> 2.2.1 根据动态库名称查询动态库的绝对路径 |
可以看到,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(【绝对路径】)
加载动态库。
到目前为止,调用栈如下:
1 | scss复制代码System.loadLibrary(libPath) |
- nativeLoad(…) 主流程源码分析
经过前面的分析,取到 so 库的绝对路径之后,最终是调用 native 方法nativeLoad(...)
加载 so 库,相关源码如下:
Runtime.java
1 | arduino复制代码-> 1.2 / 2.2.2 / 2.3.3 |
1 | kotlin复制代码JNIEXPORT jstring JNICALL |
最终调用到:java_vm_ext.cc
1 | ini复制代码共享库列表 |
上面的代码已经非常简化了,主要关注以下几点:
- 1、检查是否已经加载过(
libraries_
记录了已经加载过的 so 库); - 2、如果已经加载过,跳过;
- 3、调用
dlopen
打开 so 库; - 4、创建共享库
SharedLibrary
,这个就是 so 库的内存表示,需要注意的是,SharedLibrary 和 ClassLoader 是有关联的(SharedLibrary 持有了 ClassLoader),这一点在卸载 so 库的时候会用到; - 5、将共享库记录到
libraries_
表中; - 6、调用 so 库中的
JNI_OnLoad
方法,返回值是jint
类型,告诉虚拟机此 so 库使用的 JNI版本
整个加载的过程:
- 卸载 so 库
JDK 没有提供直接卸载 so 库的方法,而是 在ClassLoader 卸载时跟随卸载,具体触发的地方在虚拟机堆执行垃圾回收的源码:
1 | rust复制代码collector::GcType Heap::CollectGarbageInternal(collector::GcType gc_type, |
这里我们只关注与共享库有关的代码,最终调用到:java_vm_ext.cc
1 | ini复制代码已简化 |
上面的代码已经非常简化了,主要关注以下几点:
- 1、遍历共享库列表
libraries_
- 2、检查关联的 ClassLoader 是否卸载(unload)
- 3、记录需要卸载的共享库
- 4、遍历需要卸载的共享库,执行
JNI_OnUnload()
,返回值是void
- 5、回收内存
- 总结
- 应试建议
1、应知晓 so 库加载到卸载的大体过程,主要分为:确定 so 库绝对路径、nativeLoad 加载进内存、ClassLoader 卸载时跟随卸载;
2、应知晓搜索 so 库的路径,分为 App 路径和系统路径
3、应知晓JNI_OnLoad
与JNI_OnUnLoad
的执行时机(分别在加载与卸载时执行)
参考资料
- 《Java中System.loadLibrary() 的执行过程》 —— WolfCS 著
- 《Android JNI 原理分析》 —— Gityuan 著
- 《loadLibrary 动态库加载过程分析》 —— Gityuan 著
推荐阅读
- 密码学 | Base64是加密算法吗?
- 算法面试题 | 回溯算法解题框架
- 算法面试题 | 链表问题总结
- Java | 带你理解 ServiceLoader 的原理与设计思想
- 计算机网络 | 图解 DNS & HTTPDNS 原理
- Android | 说说从 android:text 到 TextView 的过程
- Android | 面试必问的 Handler,你确定不看看?
- Android | 带你探究 LayoutInflater 布局解析原理
- Android | View & Fragment & Window 的 getContext() 一定返回 Activity 吗?
感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的GitHub!
本文转载自: 掘金