Android?動態(tài)加載?so實(shí)現(xiàn)示例詳解
背景
對于一個(gè)普通的android應(yīng)用來說,so庫的占比通常都是巨高不下的,因?yàn)槲覀儫o可避免的在開發(fā)中遇到各種各樣需要用到native的需求,所以so庫的動態(tài)化可以減少極大的包體積,自從2020騰訊的bugly團(tuán)隊(duì)發(fā)部關(guān)于動態(tài)化so的相關(guān)文章后,已經(jīng)過去兩年了,相關(guān)文章,經(jīng)過兩年的考驗(yàn),實(shí)際上so動態(tài)加載也是非常成熟的一項(xiàng)技術(shù)了。
但是很遺憾,許多公司都還沒有這方面的涉略又或者說不知道從哪里開始進(jìn)行,因?yàn)閟o動態(tài)其實(shí)涉及到下載,so版本管理,動態(tài)加載實(shí)現(xiàn)等多方面,我們不妨拋開這些額外的東西,從最本質(zhì)的so動態(tài)加載出發(fā)吧!這里是本次的例子,我把它命名為sillyboy,歡迎pr還有后續(xù)點(diǎn)贊呀!
so動態(tài)加載介紹
動態(tài)加載,其實(shí)就是把我們的so庫在打包成apk的時(shí)候剔除,在合適的時(shí)候通過網(wǎng)絡(luò)包下載的方式,通過一些手段,在運(yùn)行的時(shí)候進(jìn)行分離加載的過程。
這里涉及到下載器,還有下載后的版本管理等等確保一個(gè)so庫被正確的加載等過程,在這里,我們不討論這些輔助的流程,我們看下怎么實(shí)現(xiàn)一個(gè)最簡單的加載流程。
從一個(gè)例子出發(fā)
我們構(gòu)建一個(gè)native工程,然后在里面編入如下內(nèi)容,下面是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_libHrary( # 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})
可以看到,我們生成了兩個(gè)so庫一個(gè)是nativecpp,還有一個(gè)是nativecpptwo(為什么要兩個(gè)呢?我們可以繼續(xù)看下文) 這里也給出最關(guān)鍵的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 層方法"); }
很簡單,就一個(gè)native方法,打印一個(gè)log即可,我們就可以在java/kotin層進(jìn)行方法調(diào)用了,即
public native void clickTest();
so庫檢索與刪除
要實(shí)現(xiàn)so的動態(tài)加載,那最起碼是要知道本項(xiàng)目過程中涉及到哪些so吧!不用擔(dān)心,我們gradle構(gòu)建的時(shí)候,就已經(jīng)提供了相應(yīng)的構(gòu)建過程,即構(gòu)建的task【 mergeDebugNativeLibs】,在這個(gè)過程中,會把一個(gè)project里面的所有native庫進(jìn)行一個(gè)收集的過程,緊接著task【stripDebugDebugSymbols】是一個(gè)符號表清除過程,如果了解native開發(fā)的朋友很容易就知道,這就是一個(gè)減少so體積的一個(gè)過程,我們不在這里詳述。
所以我們很容易想到,我們只要在這兩個(gè)task中插入一個(gè)自定義的task,用于遍歷和刪除就可以實(shí)現(xiàn)so的刪除化了,所以就很容易寫出這樣的代碼
ext { deleteSoName = ["libnativecpptwo.so","libnativecpp.so"] } // 這個(gè)是初始化 -配置 -執(zhí)行階段中,配置階段執(zhí)行的任務(wù)之一,完成afterEvaluate就可以得到所有的tasks,從而可以在里面插入我們定制化的數(shù)據(jù) task(dynamicSo) { }.doLast { println("dynamicSo insert!!!! ") //projectDir 在哪個(gè)project下面,projectDir就是哪個(gè)路徑 print(getRootProject().findAll()) def file = new File("${projectDir}/build/intermediates/merged_native_libs/debug/out/lib") //默認(rèn)刪除所有的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) } }
可以看到,我們定義了一個(gè)自定義task dynamicSo,它的執(zhí)行是在afterEvaluate中定義的,并且依賴于mergeDebugNativeLibs,而stripDebugDebugSymbols就依賴于我們生成的dynamicSo,達(dá)到了一個(gè)插入操作。
那么為什么要在afterEvaluate中執(zhí)行呢?那是因?yàn)閍ndroid插件是在配置階段中才生成的mergeDebugNativeLibs等任務(wù),原本的gradle構(gòu)建是不存在這樣一個(gè)任務(wù)的,所以我們才需要在配置完所有task之后,才進(jìn)行的插入,我們可以看一下gradle的生命周期
通過對條件檢索,我們就刪除掉了我們想要的so,即ibnativecpptwo.so與libnativecpp.so。
動態(tài)加載so
根據(jù)上文檢索出來的兩個(gè)so,我們就可以在項(xiàng)目中上傳到自己的后端中,然后通過網(wǎng)絡(luò)下載到用戶的手機(jī)上,這里我們就演示一下即可,我們就直接放在data目錄下面吧
真實(shí)的項(xiàng)目過程中,應(yīng)該要有校驗(yàn)操作,比如md5校驗(yàn)或者可以解壓等等操作,這里不是重點(diǎn),我們就直接略過啦!
那么,怎么把一個(gè)so庫加載到我們本來的apk中呢?這里是so原本的加載過程,可以看到,系統(tǒng)是通過classloader檢索native目錄是否存在so庫進(jìn)行加載的,那我們反射一下,把我們自定義的path加入進(jìn)行不就可以了嗎?這里采用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); } }
我們在原本的檢索路徑中,在最前面,即數(shù)組為0的位置加入了我們的檢索路徑,這樣一來classloader在查找我們已經(jīng)動態(tài)化的so庫的時(shí)候,就能夠找到!
結(jié)束了嗎?
一般的so庫,比如不依賴其他的so的時(shí)候,直接這樣加載就沒問題了,但是如果存在著依賴的so庫的話,就不行了!相信大家在看其他的博客的時(shí)候就能看到,是因?yàn)镹amespace的問題。具體是我們動態(tài)庫加載的過程中,如果需要依賴其他的動態(tài)庫,那么就需要一個(gè)鏈接的過程對吧!
這里的實(shí)現(xiàn)就是Linker,Linker 里檢索的路徑在創(chuàng)建 ClassLoader 實(shí)例后就被系統(tǒng)通過 Namespace 機(jī)制綁定了,當(dāng)我們注入新的路徑之后,雖然 ClassLoader 里的路徑增加了,但是 Linker 里 Namespace 已經(jīng)綁定的路徑集合并沒有同步更新,所以出現(xiàn)了 libxxx.so 文件(當(dāng)前的so)能找到,而依賴的so 找不到的情況。bugly文章
很多實(shí)現(xiàn)都采用了Tinker的實(shí)現(xiàn),既然我們系統(tǒng)的classloader是這樣,那么我們在合適的時(shí)候把這個(gè)替換掉不就可以了嘛!當(dāng)然bugly團(tuán)隊(duì)就是這樣做的,但是筆者認(rèn)為,替換一個(gè)classloader顯然對于一個(gè)普通應(yīng)用來說,成本還是太大了,而且兼容性風(fēng)險(xiǎn)也挺高的,當(dāng)然,還有很多方式,比如采用Relinker這個(gè)庫自定義我們加載的邏輯。
為了不冷飯熱炒,嘿嘿,雖然我也喜歡吃炒飯(手動狗頭),這里我們就不采用替換classloader的方式,而是采用跟relinker的思想,去進(jìn)行加載!
具體的可以看到sillyboy的實(shí)現(xiàn),其實(shí)就不依賴relinker跟tinker,因?yàn)槲野殃P(guān)鍵的拷貝過來了,哈哈哈,好啦,我們看下怎么實(shí)現(xiàn)吧!不過在此這前,我們需要了解一些前置知識
ELF文件
我們的so庫,本質(zhì)就是一個(gè)elf文件,那么so庫也符合elf文件的格式,ELF文件由4部分組成,分別是ELF頭(ELF header)、程序頭表(Program header table)、節(jié)(Section)和節(jié)頭表(Section header table)。
實(shí)際上,一個(gè)文件中不一定包含全部內(nèi)容,而且它們的位置也未必如同所示這樣安排,只有ELF頭的位置是固定的,其余各部分的位置、大小等信息由ELF頭中的各項(xiàng)值來決定。
那么我們so中,如果依賴于其他的so,那么這個(gè)信息存在哪里呢?。繘]錯,它其實(shí)也存在elf文件中,不然鏈接器怎么找嘛,它其實(shí)就存在.dynamic段中,所以我們只要找打dynamic段的偏移,就能到dynamic中,而被依賴的so的信息,其實(shí)就存在里面啦 我們可以用readelf(ndk中就有toolchains目錄后) 查看,readelf -d nativecpptwo.so 這里的 -d 就是查看dynamic段的意思
這里面涉及到動態(tài)加載so的知識,可以推薦大家一本書,叫做程序員的自我修養(yǎng)-鏈接裝載與庫這里就畫個(gè)初略圖
我們再看下本質(zhì),dynamic結(jié)構(gòu)體如下,定義在elf.h中
typedef struct{ Elf32_Sword d_tag; union{ Elf32_Addr d_ptr; .... } }
當(dāng)d_tag的數(shù)值為DT_NEEDED的時(shí)候,就代表著依賴的共享對象文件,d_ptr表示所依賴的共享對象的文件名??吹竭@里讀者們已經(jīng)知道了,如果我們知道了文件名,不就可以再用System.loadLibrary去加載這個(gè)文件名確定的so了嘛!不用替換classloader就能夠保證被依賴的庫先加載!我們可以再總結(jié)一下這個(gè)方案的原理,如圖
比如我們要加載so3,我們就需要先加載so2,如果so2存在依賴,那我們就調(diào)用System.loadLibrary先加載so1,這個(gè)時(shí)候so1就不存在依賴項(xiàng)了,就不需要再調(diào)用Linker去查找其他so庫了。我們最終方案就是,只要能夠解析對應(yīng)的elf文件,然后找偏移,找到需要的目標(biāo)項(xiàng)(DT_NEED)所對應(yīng)的數(shù)值(即被依賴的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; }
擴(kuò)展
我們到這里,就能夠解決so庫的動態(tài)加載的相關(guān)問題了,那么還有人可能會問,項(xiàng)目中是會存在多處System.load方式的,如果加載的so還不存在怎么辦?
比如還在下載當(dāng)中,其實(shí)很簡單,這個(gè)時(shí)候我們字節(jié)碼插樁就派上用場了,只要我們把System.load替換為我們自定義的加載so邏輯,進(jìn)行一定的邏輯處理就可以了,嘿嘿,因?yàn)楣P者之前就有寫一個(gè)字節(jié)碼插樁的庫的介紹,所以在本次就不重復(fù)了,可以看 https://github.com/TestPlanB/Spider ,同時(shí)也可以用其他的字節(jié)碼插樁框架實(shí)現(xiàn),相信這不是一個(gè)問題。
總結(jié)
看到這里的讀者,相信也能夠明白動態(tài)加載so的步驟了
源代碼 https://github.com/TestPlanB/SillyBoy
以上就是Android 動態(tài)加載 so實(shí)現(xiàn)示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Android 動態(tài)加載 so的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android scrollTo和scrollBy方法使用解析
在一個(gè)View中,系統(tǒng)提供了scrollTo、scrollBy兩種方式來改變一個(gè)View的位置,下面通過本文給大家介紹Android scrollTo和scrollBy方法使用解析,需要的朋友參考下吧2018-01-01android使用ItemDecoration給RecyclerView 添加水印
本篇文章主要介紹了android使用ItemDecoration給RecyclerView 添加水印,介紹了自定義Drawable來完成水印圖片和使用ItemDecoration來布局水印,有興趣的可以了解一下。2017-02-02Android編程實(shí)現(xiàn)的EditText彈出打開和關(guān)閉工具類
這篇文章主要介紹了Android編程實(shí)現(xiàn)的EditText彈出打開和關(guān)閉工具類,涉及Android輸入框EditText彈出打開和關(guān)閉功能簡單實(shí)現(xiàn)技巧,需要的朋友可以參考下2018-02-02Android開發(fā)中TextView 實(shí)現(xiàn)右上角跟隨文本動態(tài)追加圓形紅點(diǎn)
這篇文章主要介紹了android textview 右上角跟隨文本動態(tài)追加圓形紅點(diǎn)的實(shí)例代碼,非常不錯,具有參考借鑒價(jià)值,需要的朋友可以參考下2016-11-11Android Studio實(shí)現(xiàn)QQ的注冊登錄和好友列表跳轉(zhuǎn)
最近做了一個(gè)項(xiàng)目,這篇文章主要介紹了Android Studio界面跳轉(zhuǎn),本次項(xiàng)目主要包含了注冊、登錄和好友列表三個(gè)界面以及之間相互跳轉(zhuǎn),感興趣的可以了解一下2021-05-05Jetpack Compose實(shí)現(xiàn)列表和動畫效果詳解
這篇文章主要為大家詳細(xì)講講Jetpack Compose實(shí)現(xiàn)列表和動畫效果的方法步驟,文中的代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2022-06-06