Android動態加載so學習

轉載的 作者是下面這個大佬
作者:Pika
鏈接:https://juejin.cn/post/7107958280097366030
來源:稀土掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

學習記錄一下

so動態加載介紹

動態加載,其實就是把我們的so庫在打包成apk的時候剔除,在合適的時候通過網絡包下載的方式,通過一些手段,在運行的時候進行分離加載的過程。這里涉及到下載器,還有下載后的版本管理等等確保一個so庫被正確的加載等過程,在這里,我們不討論這些輔助的流程,我們看下怎么實現一個最簡單的加載流程。

image.png

從一個例子出發

我們構建一個native工程,然后在里面編入如下內容,下面是cmake

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.18.1)

# Declares and names the project.

project("nativecpp")

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
        nativecpp

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        native-lib.cpp)

add_library(
        nativecpptwo
        SHARED
        test.cpp

)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
        log-lib

        # Specifies the name of the NDK library that
        # you want CMake to locate.
        log)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        nativecpp

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

target_link_libraries( # Specifies the target library.
        nativecpptwo

        # Links the target library to the log library
        # included in the NDK.
        nativecpp
        ${log-lib})
復制代碼

可以看到,我們生成了兩個so庫一個是nativecpp,還有一個是nativecpptwo(為什么要兩個呢?我們可以繼續看下文) 這里也給出最關鍵的test.cpp代碼


#include <jni.h>
#include <string>
#include<android/log.h>

extern "C"
JNIEXPORT void JNICALL
Java_com_example_nativecpp_MainActivity_clickTest(JNIEnv *env, jobject thiz) {
    // 在這里打印一句話
    __android_log_print(ANDROID_LOG_INFO,"hello"," native 層方法");

}
復制代碼

很簡單,就一個native方法,打印一個log即可,我們就可以在java/kotin層進行方法調用了,即

public native void clickTest();
復制代碼

so庫檢索與刪除

要實現so的動態加載,那最起碼是要知道本項目過程中涉及到哪些so吧!不用擔心,我們gradle構建的時候,就已經提供了相應的構建過程,即構建的task【 mergeDebugNativeLibs】,在這個過程中,會把一個project里面的所有native庫進行一個收集的過程,緊接著task【stripDebugDebugSymbols】是一個符號表清除過程,如果了解native開發的朋友很容易就知道,這就是一個減少so體積的一個過程,我們不在這里詳述。所以我們很容易想到,我們只要在這兩個task中插入一個自定義的task,用于遍歷和刪除就可以實現so的刪除化了,所以就很容易寫出這樣的代碼


ext {
    deleteSoName = ["libnativecpptwo.so","libnativecpp.so"]
}
// 這個是初始化 -配置 -執行階段中,配置階段執行的任務之一,完成afterEvaluate就可以得到所有的tasks,從而可以在里面插入我們定制化的數據
task(dynamicSo) {
}.doLast {
    println("dynamicSo insert!!!! ")
    //projectDir 在哪個project下面,projectDir就是哪個路徑
    print(getRootProject().findAll())

    def file = new File("${projectDir}/build/intermediates/merged_native_libs/debug/out/lib")
    //默認刪除所有的so庫
    if (file.exists()) {
        file.listFiles().each {
            if (it.isDirectory()) {
                it.listFiles().each {
                    target ->
                        print("file ${target.name}")
                        def compareName = target.name
                        deleteSoName.each {
                            if (compareName.contains(it)) {
                                target.delete()
                            }
                        }
                }
            }
        }
    } else {
        print("nil")
    }
}
afterEvaluate {
    print("dynamicSo task start")
    def customer = tasks.findByName("dynamicSo")
    def merge = tasks.findByName("mergeDebugNativeLibs")
    def strip = tasks.findByName("stripDebugDebugSymbols")
    if (merge != null || strip != null) {
        customer.mustRunAfter(merge)
        strip.dependsOn(customer)
    }

}
復制代碼

可以看到,我們定義了一個自定義task dynamicSo,它的執行是在afterEvaluate中定義的,并且依賴于mergeDebugNativeLibs,而stripDebugDebugSymbols就依賴于我們生成的dynamicSo,達到了一個插入操作。那么為什么要在afterEvaluate中執行呢?那是因為android插件是在配置階段中才生成的mergeDebugNativeLibs等任務,原本的gradle構建是不存在這樣一個任務的,所以我們才需要在配置完所有task之后,才進行的插入,我們可以看一下gradle的生命周期

image.png

通過對條件檢索,我們就刪除掉了我們想要的so,即ibnativecpptwo.so與libnativecpp.so。

動態加載so

根據上文檢索出來的兩個so,我們就可以在項目中上傳到自己的后端中,然后通過網絡下載到用戶的手機上,這里我們就演示一下即可,我們就直接放在data目錄下面吧

真實的項目過程中,應該要有校驗操作,比如md5校驗或者可以解壓等等操作,這里不是重點,我們就直接略過啦!

那么,怎么把一個so庫加載到我們本來的apk中呢?這里是so原本的加載過程,可以看到,系統是通過classloader檢索native目錄是否存在so庫進行加載的,那我們反射一下,把我們自定義的path加入進行不就可以了嗎?這里采用tinker一樣的思路,在我們的classloader中加入so的檢索路徑即可,比如

private static final class V25 {
    private static void install(ClassLoader classLoader, File folder)  throws Throwable {
        final Field pathListField = ShareReflectUtil.findField(classLoader, "pathList");
        final Object dexPathList = pathListField.get(classLoader);

        final Field nativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "nativeLibraryDirectories");

        List<File> origLibDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
        if (origLibDirs == null) {
            origLibDirs = new ArrayList<>(2);
        }
        final Iterator<File> libDirIt = origLibDirs.iterator();
        while (libDirIt.hasNext()) {
            final File libDir = libDirIt.next();
            if (folder.equals(libDir)) {
                libDirIt.remove();
                break;
            }
        }
        origLibDirs.add(0, folder);

        final Field systemNativeLibraryDirectories = ShareReflectUtil.findField(dexPathList, "systemNativeLibraryDirectories");
        List<File> origSystemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
        if (origSystemLibDirs == null) {
            origSystemLibDirs = new ArrayList<>(2);
        }

        final List<File> newLibDirs = new ArrayList<>(origLibDirs.size() + origSystemLibDirs.size() + 1);
        newLibDirs.addAll(origLibDirs);
        newLibDirs.addAll(origSystemLibDirs);

        final Method makeElements = ShareReflectUtil.findMethod(dexPathList, "makePathElements", List.class);

        final Object[] elements = (Object[]) makeElements.invoke(dexPathList, newLibDirs);

        final Field nativeLibraryPathElements = ShareReflectUtil.findField(dexPathList, "nativeLibraryPathElements");
        nativeLibraryPathElements.set(dexPathList, elements);
    }
}
復制代碼

我們在原本的檢索路徑中,在最前面,即數組為0的位置加入了我們的檢索路徑,這樣一來classloader在查找我們已經動態化的so庫的時候,就能夠找到!

結束了嗎?

一般的so庫,比如不依賴其他的so的時候,直接這樣加載就沒問題了,但是如果存在著依賴的so庫的話,就不行了!相信大家在看其他的博客的時候就能看到,是因為Namespace的問題。具體是我們動態庫加載的過程中,如果需要依賴其他的動態庫,那么就需要一個鏈接的過程對吧!這里的實現就是Linker,Linker 里檢索的路徑在創建 ClassLoader 實例后就被系統通過 Namespace 機制綁定了,當我們注入新的路徑之后,雖然 ClassLoader 里的路徑增加了,但是 Linker 里 Namespace 已經綁定的路徑集合并沒有同步更新,所以出現了 libxxx.so 文件(當前的so)能找到,而依賴的so 找不到的情況。bugly文章

很多實現都采用了Tinker的實現,既然我們系統的classloader是這樣,那么我們在合適的時候把這個替換掉不就可以了嘛!當然bugly團隊就是這樣做的,但是筆者認為,替換一個classloader顯然對于一個普通應用來說,成本還是太大了,而且兼容性風險也挺高的,當然,還有很多方式,比如采用Relinker這個庫自定義我們加載的邏輯。

為了不冷飯熱炒,嘿嘿,雖然我也喜歡吃炒飯(手動狗頭),這里我們就不采用替換classloader的方式,而是采用跟relinker的思想,去進行加載!具體的可以看到sillyboy的實現,其實就不依賴relinker跟tinker,因為我把關鍵的拷貝過來了,哈哈哈,好啦,我們看下怎么實現吧!不過在此這前,我們需要了解一些前置知識

ELF文件

我們的so庫,本質就是一個elf文件,那么so庫也符合elf文件的格式,ELF文件由4部分組成,分別是ELF頭(ELF header)、程序頭表(Program header table)、節(Section)和節頭表(Section header table)。實際上,一個文件中不一定包含全部內容,而且它們的位置也未必如同所示這樣安排,只有ELF頭的位置是固定的,其余各部分的位置、大小等信息由ELF頭中的各項值來決定。

[圖片上傳失敗...(image-715313-1663851710162)]

那么我們so中,如果依賴于其他的so,那么這個信息存在哪里呢!?沒錯,它其實也存在elf文件中,不然鏈接器怎么找嘛,它其實就存在.dynamic段中,所以我們只要找打dynamic段的偏移,就能到dynamic中,而被依賴的so的信息,其實就存在里面啦 我們可以用readelf(ndk中就有toolchains目錄后) 查看,readelf -d nativecpptwo.so 這里的 -d 就是查看dynamic段的意思

這里面涉及到動態加載so的知識,可以推薦大家一本書,叫做程序員的自我修養-鏈接裝載與庫這里就畫個初略圖 [圖片上傳失敗...(image-339424-1663851710162)]

我們再看下本質,dynamic結構體如下,定義在elf.h中

typedef struct{
Elf32_Sword d_tag;
union{
Elf32_Addr d_ptr;
....
}
}
復制代碼

當d_tag的數值為DT_NEEDED的時候,就代表著依賴的共享對象文件,d_ptr表示所依賴的共享對象的文件名。看到這里讀者們已經知道了,如果我們知道了文件名,不就可以再用System.loadLibrary去加載這個文件名確定的so了嘛!不用替換classloader就能夠保證被依賴的庫先加載!我們可以再總結一下這個方案的原理,如圖

比如我們要加載so3,我們就需要先加載so2,如果so2存在依賴,那我們就調用System.loadLibrary先加載so1,這個時候so1就不存在依賴項了,就不需要再調用Linker去查找其他so庫了。我們最終方案就是,只要能夠解析對應的elf文件,然后找偏移,找到需要的目標項(DT_NEED)所對應的數值(即被依賴的so文件名)就可以了

public List<String> parseNeededDependencies() throws IOException {
    channel.position(0);
    final List<String> dependencies = new ArrayList<String>();
    final Header header = parseHeader();
    final ByteBuffer buffer = ByteBuffer.allocate(8);
    buffer.order(header.bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN);

    long numProgramHeaderEntries = header.phnum;
    if (numProgramHeaderEntries == 0xFFFF) {
        /**
         * Extended Numbering
         *
         * If the real number of program header table entries is larger than
         * or equal to PN_XNUM(0xffff), it is set to sh_info field of the
         * section header at index 0, and PN_XNUM is set to e_phnum
         * field. Otherwise, the section header at index 0 is zero
         * initialized, if it exists.
         **/
        final SectionHeader sectionHeader = header.getSectionHeader(0);
        numProgramHeaderEntries = sectionHeader.info;
    }

    long dynamicSectionOff = 0;
    for (long i = 0; i < numProgramHeaderEntries; ++i) {
        final ProgramHeader programHeader = header.getProgramHeader(i);
        if (programHeader.type == ProgramHeader.PT_DYNAMIC) {
            dynamicSectionOff = programHeader.offset;
            break;
        }
    }

    if (dynamicSectionOff == 0) {
        // No dynamic linking info, nothing to load
        return Collections.unmodifiableList(dependencies);
    }

    int i = 0;
    final List<Long> neededOffsets = new ArrayList<Long>();
    long vStringTableOff = 0;
    DynamicStructure dynStructure;
    do {
        dynStructure = header.getDynamicStructure(dynamicSectionOff, i);
        if (dynStructure.tag == DynamicStructure.DT_NEEDED) {
            neededOffsets.add(dynStructure.val);
        } else if (dynStructure.tag == DynamicStructure.DT_STRTAB) {
            vStringTableOff = dynStructure.val; // d_ptr union
        }
        ++i;
    } while (dynStructure.tag != DynamicStructure.DT_NULL);

    if (vStringTableOff == 0) {
        throw new IllegalStateException("String table offset not found!");
    }

    // Map to file offset
    final long stringTableOff = offsetFromVma(header, numProgramHeaderEntries, vStringTableOff);
    for (final Long strOff : neededOffsets) {
        dependencies.add(readString(buffer, stringTableOff + strOff));
    }

    return dependencies;
}
復制代碼

擴展

我們到這里,就能夠解決so庫的動態加載的相關問題了,那么還有人可能會問,項目中是會存在多處System.load方式的,如果加載的so還不存在怎么辦?比如還在下載當中,其實很簡單,這個時候我們字節碼插樁就派上用場了,只要我們把System.load替換為我們自定義的加載so邏輯,進行一定的邏輯處理就可以了,嘿嘿,因為筆者之前就有寫一個字節碼插樁的庫的介紹,所以在本次就不重復了,可以看Sipder,同時也可以用其他的字節碼插樁框架實現,相信這不是一個問題。

作者:Pika
鏈接:https://juejin.cn/post/7107958280097366030
來源:稀土掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,165評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,595評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,435評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,943評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,175評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,713評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,599評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,788評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,034評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,408評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,747評論 2 370

推薦閱讀更多精彩內容