「NDK 路線」| so 庫加載到卸載的全過程

前言

  • 在 JNI 開發(fā)中,必然需要用到 so 庫褐啡,那么你清楚 so 庫從加載到卸載的全過程嗎罩锐?奉狈;
  • 在這篇文章里,我將帶你建立對 so 庫從加載進內(nèi)存到卸載整個過程的理解唯欣。另外嘹吨,文末的應試建議也不要錯過哦,如果能幫上忙境氢,請務(wù)必點贊加關(guān)注蟀拷,這真的對我非常重要碰纬。

相關(guān)文章


目錄


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);

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 調(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:

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 根據(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);

Runtime.c

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í)行垃圾回收的源碼:

heap.cc

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_OnLoadJNI_OnUnLoad的執(zhí)行時機(分別在加載與卸載時執(zhí)行)

參考資料

創(chuàng)作不易扶供,你的「三連」是丑丑最大的動力筛圆,我們下次見!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末椿浓,一起剝皮案震驚了整個濱河市太援,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌扳碍,老刑警劉巖提岔,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異笋敞,居然都是意外死亡唧垦,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門液样,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人巧还,你說我怎么就攤上這事鞭莽。” “怎么了麸祷?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵澎怒,是天一觀的道長。 經(jīng)常有香客問我阶牍,道長喷面,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任走孽,我火速辦了婚禮惧辈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘磕瓷。我一直安慰自己盒齿,他們只是感情好念逞,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著边翁,像睡著了一般翎承。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上符匾,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天叨咖,我揣著相機與錄音,去河邊找鬼啊胶。 笑死甸各,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的创淡。 我是一名探鬼主播痴晦,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼琳彩!你這毒婦竟也來了誊酌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤露乏,失蹤者是張志新(化名)和其女友劉穎碧浊,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瘟仿,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡箱锐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了劳较。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片驹止。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖观蜗,靈堂內(nèi)的尸體忽然破棺而出臊恋,到底是詐尸還是另有隱情,我是刑警寧澤墓捻,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布抖仅,位于F島的核電站,受9級特大地震影響砖第,放射性物質(zhì)發(fā)生泄漏撤卢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一梧兼、第九天 我趴在偏房一處隱蔽的房頂上張望放吩。 院中可真熱鬧,春花似錦羽杰、人聲如沸屎慢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽腻惠。三九已至环肘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間集灌,已是汗流浹背悔雹。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留欣喧,地道東北人腌零。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像唆阿,于是被迫代替她去往敵國和親益涧。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359