前言
- 在 JNI 開發(fā)中,必然需要用到 so 庫褐啡,那么你清楚 so 庫從加載到卸載的全過程嗎罩锐?奉狈;
- 在這篇文章里,我將帶你建立對 so 庫從加載進內(nèi)存到卸載整個過程的理解唯欣。另外嘹吨,文末的應試建議也不要錯過哦,如果能幫上忙境氢,請務(wù)必點贊加關(guān)注蟀拷,這真的對我非常重要碰纬。
相關(guān)文章
- 《NDK | 說說 so 庫從加載到卸載的全過程》
- 《NDK | 帶你梳理 JNI 函數(shù)注冊的方式和時機》
- 《NDK | 帶你探究 getProperty() 獲取系統(tǒng)屬性原理》
- 《NDK | 一篇文章帶你點亮 JNI 開發(fā)基石符文》(快寫好了)
- 《NDK | 一篇文章開啟你的 NDK 技能樹》(真的快寫好了)
目錄
1. 獲取 so 庫
關(guān)于 獲取 so 庫的具體步驟,我在這篇文章里討論问芬,《NDK | 一篇文章開啟你的 NDK 技能樹》悦析,請關(guān)注。通常來說此衅,最終生成的 so 庫命名為lib[name].so
强戴,例如系統(tǒng)內(nèi)置的 so 庫:
2. 加載 so 庫
首先,讓我們看看加載 so 庫的入口挡鞍,加載動態(tài)庫需要使用System.load(...)
或 System.loadLibrary(...)
骑歹。通常來說,都會放在static {}
中執(zhí)行墨微。
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()
返回的是加載調(diào)用者使用的 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 調(diào)用 nativeLoad(【絕對路徑】) 加載動態(tài)庫
String error = nativeLoad(filename, fromClass.getClassLoader());
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
}
可以看到翘县,Runtime#load0(...)
的邏輯比較簡單:
- 1.1 確保參數(shù)
filename
是一個絕對路徑 - 1.2 調(diào)用
nativeLoad(【絕對路徑】)
加載動態(tài)庫最域,這個方法我在 第 3 節(jié) nativeLoad(...) 主流程源碼分析 說。
2.2 Runtime#loadLibrary0(...) 源碼分析
Runtime.java
-> 2(已簡化)
synchronized void loadLibrary0(ClassLoader loader, String libname) {
2.1 檢查是否出現(xiàn)路徑分隔符
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 根據(jù)動態(tài)庫名稱查詢動態(tài)庫的絕對路徑
String filename = loader.findLibrary(libraryName);
if (filename == null) {
throw new UnsatisfiedLinkError(...);
}
2.2.2 調(diào)用 nativeLoad(【絕對路徑】) 加載動態(tài)庫
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 調(diào)用 nativeLoad(【絕對路徑】) 加載動態(tài)庫
String error = nativeLoad(candidate, loader);
if (error == null) {
return
}
}
throw new UnsatisfiedLinkError(...);
}
可以看到锈麸,Runtime#loadLibrary0(...)
主要分為 ClassLoader 為非空與為空兩種情況镀脂。
先看 ClassLoader 非空的情況:
- 2.2.1 調(diào)用
ClassLoader#findLibrary(libraryName)
查詢動態(tài)庫的絕對路徑,這個方法我后文再說忘伞。 - 2.2.2 調(diào)用
nativeLoad(【絕對路徑】)
加載動態(tài)庫
再看下 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 調(diào)用 native 方法
System.mapLibraryName()
匿刮,拼接 lib 前綴與.so 后綴 - 2.3.2 調(diào)用
System.getProperty("java.library.path")
獲取系統(tǒng) so 庫存儲路徑 - 2.3.3 遍歷每個 so 庫存儲路徑,拼接除動態(tài)庫的絕對路徑探颈,調(diào)用
nativeLoad(【絕對路徑】)
加載動態(tài)庫
關(guān)于
System.getProperty("java.library.path")
的源碼分析熟丸,在我之前寫過的一篇文章里講過:《NDK | 帶你探究 getProperty() 獲取系統(tǒng)屬性原理》,這里我簡單復述一下:1伪节、
"java.library.path"
這個屬性是由運行環(huán)境管理的光羞;
2、對于 64 位系統(tǒng)怀大,返回的是"/system/lib64" 纱兑、 "/vendor/lib64"
;
3化借、對于 32 位系統(tǒng)潜慎,返回的是"/system/lib" 、 "/vendor/lib"
。
可以看到铐炫,對于 ClassLoader 非空和為空兩種情況垒手,其實最后都需要調(diào)用nativeLoad(【絕對路徑】)
加載動態(tài)庫,這其實和Runtime#load0(...)
的邏輯一致倒信。這個方法我在 第 3 節(jié) nativeLoad(...) 主流程源碼分析 分析科贬。
2.3 ClassLoader#findLibrary(libraryName) 源碼分析
對了,在前面講到 ClassLoader 非空的情況時鳖悠,ClassLoader#findLibrary(libraryName)
還沒有分析榜掌,現(xiàn)在講下。在 Android 系統(tǒng)中乘综,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 根據(jù)動態(tài)庫名稱查詢動態(tài)庫的絕對路徑
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);
系統(tǒng)的 so 庫路徑("java.library.path"))
this.systemNativeLibraryDirectories = splitPaths(System.getProperty("java.library.path"), true);
記錄 app 和系統(tǒng)的 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做入、系統(tǒng) so 庫存儲路徑(
/system/lib64冒晰、/vendor/lib64
)
2.4 小結(jié)
最后,總結(jié)System.load(...)
或System.loadLibrary(...)
的異同:
不同點:
-
System.load(...)
指定的是 so 庫的絕對路徑竟块,只會在該路徑搜索 so 庫壶运; -
System.loadLibrary(...)
指定的是 so 庫的名稱,查找時會自動拼接 lib 前綴和 .so 后綴浪秘,并在 app 路徑和系統(tǒng)路徑搜索蒋情。
共同點:
- 兩個方法最終都得到一個絕對路徑,并調(diào)用 native 方法
nativeLoad(【絕對路徑】)
加載動態(tài)庫耸携。
到目前為止棵癣,調(diào)用棧如下:
System.loadLibrary(libPath)
-> Runtime.load0(libPath)
-> nativeLoad(libPath)
System.loadLibrary(libName)
-> Runtime.loadLibrary0(libNane)
-> ClassLoader#findLibrary(libName)-> DexPathList#findLibrary(libName)
-> nativeLoad(libPath)
3. nativeLoad(...) 主流程源碼分析
經(jīng)過前面的分析,取到 so 庫的絕對路徑之后夺衍,最終是調(diào)用 native 方法nativeLoad(...)
加載 so 庫狈谊,相關(guān)源碼如下:
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);
}
最終調(diào)用到: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、檢查是否已經(jīng)加載過
library = libraries_->Get(path);
2、已經(jīng)加載過河劝,跳過
if (library != nullptr) {
...
return true;
}
3壁榕、調(diào)用 dlopen 打開 so 庫
void* handle = dlopen(path,RTLD_NOW);
4、創(chuàng)建共享庫
std::unique_ptr<SharedLibrary> new_library(
new SharedLibrary(env,
self,
path,
handle,
needs_native_bridge,
關(guān)注點:共享庫中持有 ClassLoader(卸載 so 庫時用到)
class_loader,
class_loader_allocator));
5丧裁、將共享庫記錄到 libraries_ 表中
libraries_->Put(path, library);
6护桦、調(diào)用 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
}
上面的代碼已經(jīng)非常簡化了,主要關(guān)注以下幾點:
- 1煎娇、檢查是否已經(jīng)加載過(
libraries_
記錄了已經(jīng)加載過的 so 庫)二庵; - 2、如果已經(jīng)加載過缓呛,跳過催享;
- 3、調(diào)用
dlopen
打開 so 庫哟绊; - 4因妙、創(chuàng)建共享庫
SharedLibrary
,這個就是 so 庫的內(nèi)存表示票髓,需要注意的是攀涵,SharedLibrary 和 ClassLoader 是有關(guān)聯(lián)的(SharedLibrary 持有了 ClassLoader),這一點在卸載 so 庫的時候會用到洽沟; - 5以故、將共享庫記錄到
libraries_
表中; - 6裆操、調(diào)用 so 庫中的
JNI_OnLoad
方法怒详,返回值是jint
類型,告訴虛擬機此 so 庫使用的 JNI版本
整個加載的過程:
4. 卸載 so 庫
JDK 沒有提供直接卸載 so 庫的方法踪区,而是 在ClassLoader 卸載時跟隨卸載昆烁,具體觸發(fā)的地方在虛擬機堆執(zhí)行垃圾回收的源碼:
collector::GcType Heap::CollectGarbageInternal(collector::GcType gc_type,
GcCause gc_cause,
bool clear_soft_references) {
...
soa.Vm()->UnloadNativeLibraries();
}
這里我們只關(guān)注與共享庫有關(guān)的代碼,最終調(diào)用到:java_vm_ext.cc
已簡化
void UnloadNativeLibraries(){
1缎岗、遍歷共享庫列表 libraries_
for (auto it = libraries_.begin(); it != libraries_.end(); ) {
SharedLibrary* const library = it->second;
2静尼、檢查關(guān)聯(lián)的 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传泊、遍歷需要卸載的共享庫茅郎,執(zhí)行 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、回收內(nèi)存
delete library;
}
}
上面的代碼已經(jīng)非常簡化了或渤,主要關(guān)注以下幾點:
- 1系冗、遍歷共享庫列表
libraries_
- 2、檢查關(guān)聯(lián)的 ClassLoader 是否卸載(unload)
- 3薪鹦、記錄需要卸載的共享庫
- 4掌敬、遍歷需要卸載的共享庫惯豆,執(zhí)行
JNI_OnUnloadFn()
,返回值是void
- 5奔害、回收內(nèi)存
5. 總結(jié)
- 應試建議
1楷兽、應知曉 so 庫加載到卸載的大體過程,主要分為:確定 so 庫絕對路徑华临、nativeLoad 加載進內(nèi)存芯杀、ClassLoader 卸載時跟隨卸載;
2雅潭、應知曉搜索 so 庫的路徑揭厚,分為 App 路徑和系統(tǒng)路徑
3、應知曉JNI_OnLoad
與JNI_OnUnLoad
的執(zhí)行時機(分別在加載與卸載時執(zhí)行)
參考資料
- 《Java中System.loadLibrary() 的執(zhí)行過程》 —— WolfCS 著
- 《Android JNI 原理分析》 —— Gityuan 著
- 《loadLibrary 動態(tài)庫加載過程分析》 —— Gityuan 著
創(chuàng)作不易扶供,你的「三連」是丑丑最大的動力筛圆,我們下次見!