Android外掛程式化總結

來源:互聯網
上載者:User

標籤:label   word   定義   編寫   複雜   osgi   它的   基礎知識   number   

瓶頸

大多數朋友開始接觸這個問題是因為 App 爆棚了,方法數超過了一個 Dex 最大方法數 65535 的上限,因而便有了外掛程式化的概念,將一個 App 劃分為多個外掛程式(Apk 或相關格式)

外掛程式化動態載入架構方案會為我們帶來多麼巨大的收益,除此之外還有諸多好處:

編譯速度提升

工程被拆分為十來個子工程之後,Android Studio編譯流程繁冗的缺點被迅速放大.

啟動速度提升

Google提供的MultiDex方案,會在主線程中執行所有dex的解壓、dexopt、載入操作,這是一個非常漫長的過程,使用者會明顯的看到長久的黑屏,更容易造成主線程的ANR,導致初次開機初始化失敗。

A/B Testing

可以獨立開發AB版本的模組,而不是將AB版本代碼寫在同一個模組中。

可選模組按需下載

?例如用於調試功能的模組可以在需要時進行下載後進行載入,減少App Size

介紹名詞

外掛程式化 – apk 分為宿主和外掛程式部分,外掛程式在需要的時候才載入進來

熱修複 – 更新的類或者外掛程式粒度較小的時候,我們會稱之為熱修複,一般用於修複bug

熱更新 – 2016 Google 的 Android Studio 推出了Instant Run 功能 同時提出了3個名詞

“熱部署” – 方法內的簡單修改,無需重啟app和Activity。 “暖部署” – app無需重啟,但是activity需要重啟,比如資源的修改。 “冷部署” – app需要重啟,比如繼承關係的改變或方法的簽名變化等。

MulitiDex開始

當 Android 系統安裝一個應用的時候,有一步是對 Dex 進行最佳化,這個過程有一個專門的工具來處理,叫 DexOpt 。DexOpt 的執行過程是在第一次載入Dex檔案的時候執行的。這個過程會產生一個 ODEX 檔案,即 Optimised Dex。執行 ODex 的效率會比直接執行 Dex 檔案的效率要高很多。

但是在早期的 Android 系統中,DexOpt 有一個問題,DexOpt 會把每一個類的方法 id 檢索起來,存在一個鏈表結 構裡面。但是這個鏈表的長度是用一個 short 類型來儲存的,導致了方法 id 的數目不能夠超過65536個。當一個項目足夠大的時候,顯然這個方法數的上限是不夠的。儘管在新版本的 Android 系統中,DexOpt 修複了這個問題,但是我們仍然需要對低版本的 Android 系統做相容。

為瞭解決方法數超限的問題,需要將該dex檔案拆成兩個或多個,為此Google官方推出了 multidex 相容包,配合 AndroidStudio 實現了一個 APK 包含多個 dex 的功能。

MulitDex 引起的問題有:

在應用安裝到手機上的時候dex檔案的安裝是複雜的(complex)有可能會因為第二個dex檔案太大導致ANR。
使用了mulitDex的App有可能在4.0(api level 14)以前的機器上無法啟動,因為Dalvik linearAlloc bug(Issue 22586) 。
使用了mulitDex的App在runtime期間有可能因為Dalvik linearAlloc limit (Issue 78035) Crash。該記憶體配置限制在 4.0版本被增大,但是5.0以下的機器上的Apps依然會存在這個限制。
主dex被dalvik虛擬機器執行時候,哪些類必須在主dex檔案裡面這個問題比較複雜。build tools 可以搞定這。

實現外掛程式化需要解決的技術點資源如何載入(資源衝突問題如何解決)代碼如何載入訪問訪問?外掛程式的管理後台包括的內容?問題1 資源如何載入方案一:

將外掛程式apk資源解壓,通過操作檔案的方式來使用,這個只是理論上可行,實際使用的時候還是有很多的問題。(主要是混淆後就懵逼了)

方案二:

重寫 Context 的getResource() getAsset() 之類的方法。資源衝突需要擴充 aapt 實現。

運行時資源的載入

平常我們使用資源,都是通過AssetManager類和Resources類來訪問的。擷取它們的方法位於Context類中。

Context.java

/** Return an AssetManager instance for your application‘s package. */public abstract AssetManager getAssets();/** Return a Resources instance for your application‘s package. */public abstract Resources getResources();

它們是兩個抽象方法,具體的實現在ContextImpl類中。ContextImpl類中初始化Resources對象後,後續Context各子類包括Activity、Service等組件就都可以通過這兩個方法讀取資源了。

ContextImpl.java

private final Resources mResources;@Overridepublic AssetManager getAssets() {   return getResources().getAssets();}@Overridepublic Resources getResources() {   return mResources;}

既然我們已經知道一個資源ID應該從哪個apk去讀取(前面在編譯期我們已經在資源ID第一個位元組標記了資源所屬的package),那麼只要我們重寫這兩個抽象方法,即可指導應用程式去正確的地方讀取資源。

至於讀取資源,AssetManager有一個隱藏方法addAssetPath,可以為AssetManager添加資源路徑。

/*** Add an additional set of assets to the asset manager.  This can be* either a directory or ZIP file.  Not for use by applications.  Returns* the cookie of the added asset, or 0 on failure.* {@hide}*/public final int addAssetPath(String path) {   synchronized (this) {       int res = addAssetPathNative(path);       makeStringBlocks(mStringBlocks);       return res;   }}

我們只需反射調用這個方法,然後把外掛程式apk的位置告訴AssetManager類,它就會根據apk內的resources.arsc和已編譯資源完成資源載入的任務了。

以上我們已經可以做到載入外掛程式資源了,但使用了一大堆定製類實現。要做到“無縫”體驗,還需要一步:使用Instrumentation來接管所有Activity、Service等組件的建立(當然也就包含了它們使用到的Resources類)。

話說Activity、Service等系統組件,都會經由android.app.ActivityThread類在主線程中執行。ActivityThread類有一個成員叫mInstrumentation,它會負責建立Activity等操作,這正是注入我們的修改資源類的最佳時機。通過篡改mInstrumentation為我們自己的InstrumentationHook,每次建立Activity的時候順手把它的mResources類偷天換日為我們的DelegateResources,以後建立的每個Activity都擁有一個懂得外掛程式、懂得委託的資源載入類啦!

當然,上述替換都會針對Application的Context來操作。

方案三:

打包後執行一個指令碼修改資源ID。

原理:

資源id是在編譯時間產生的,其產生的規則是0xPPTTNNNN,PP段,是用來標記apk的,預設情況下系統資源PP是01,應用程式的PP是07。TT段,是用來標記資源類型的,比標、布局等,相同的類型TT值相同,但是同一個TT值不代表同一種資源,例如這次編譯的時候可能使用03作為layout的TT,那下次編譯的時候可能會使用06作為TT的值,具體使用那個值,實際上和當前APP使用的資源類型的個數是相關聯的。NNNN則是某種資源類型的資源id,預設從1開始,依次累加。

那麼我們要解決資源id問題,就可從TT的值開始入手,只要將每次編譯時間的TT值固定,即可是資源id達到分組的效果,從而避免重複。例如將宿主程式的layout資源的TT固定為33,將外掛程式程式資源的layout的TT值固定為03(也可不對外掛程式程式的資源id做任何處理,使其使用編譯出來的原生的值), 即可解決資源id重複的問題了。

固定資源id的TT值的辦法也非常簡單,提供一份public.xml,在public.xml中指定什麼資源類型以什麼TT值開頭即可

還有一個方法是通過定製過的aapt在編譯時間指定外掛程式的PP段的值來實現分組,重寫過的aapt指定PP段來實現id分組。

問題2 代碼如何載入訪問

類的載入相對比較簡單。與Java程式的運行時classpath概念類似,Android的系統預設類載入器PathClassLoader也有一個成員pathList,顧名思義它從本質來說是一個List,運行時會從其間的每一個dex路徑中尋找需要載入的類。既然是個List,一定就會想到,給它追加一堆dex路徑不就得了?實際上,Google官方推出的MultiDex庫就是用以上原理實現的。下面程式碼片段展示了修改pathList路徑的細節:

MultiDex.java

private static void install(ClassLoader loader, List<File> additionalClassPathEntries,     File optimizedDirectory)             throws IllegalArgumentException, IllegalAccessException,             NoSuchFieldException, InvocationTargetException, NoSuchMethodException {    /* The patched class loader is expected to be a descendant of    * dalvik.system.BaseDexClassLoader. We modify its    * dalvik.system.DexPathList pathList field to append additional DEX    * file entries.    */    Field pathListField = findField(loader, "pathList");    Object dexPathList = pathListField.get(loader);    expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,         new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));}
Java ClassLoader基礎1. ClassLoader 的基礎知識

無論是 JVM 還是 Dalvik 都是通過 ClassLoader 去載入所需要的類,而 ClassLoader 載入類的方式常稱為雙親委託,ClassLoader.java 具體代碼如下:
Java

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {    Class<?> clazz = findLoadedClass(className);    if (clazz == null) {        try {            clazz = parent.loadClass(className, false);        } catch (ClassNotFoundException e) {            // Don‘t want to see this.        }        if (clazz == null) {            clazz = findClass(className);        }    }    return clazz;}
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {    Class<?> clazz = findLoadedClass(className);    if (clazz == null) {        try {            clazz = parent.loadClass(className, false);        } catch (ClassNotFoundException e) {            // Don‘t want to see this.        }        if (clazz == null) {            clazz = findClass(className);        }    }    return clazz;}

從上面載入類的順序中我們可以知道,loadClass 會先看這個類是不是已經被 loaded 過,沒有的話則去他的 parent 去找,如此遞迴,稱之為雙親委託。

2. 動態載入 Jar

Java 中動態載入 Jar 比較簡單,如下:

URL[] urls = new URL[] {new URL("file:libs/jar1.jar")};URLClassLoader loader = new URLClassLoader(urls, parentLoader);

表示載入 libs 下面的 jar1.jar,其中 parentLoader 就是上面1中的 parent,可以為當前的 ClassLoader。

3. ClassLoader 隔離問題

大家覺得一個運行程式中有沒有可能同時存在兩個包名和類名完全一致的類?
JVM 及 Dalvik 對類唯一的識別是 ClassLoader id + PackageName + ClassName,所以一個運行程式中是有可能存在兩個包名和類名完全一致的類的。並且如果這兩個”類”不是由一個 ClassLoader 載入,是無法將一個類的樣本強轉為另外一個類的,這就是 ClassLoader 隔離。 如 Android 中碰到如下異常

java.lang.ClassCastException: android.support.v4.view.ViewPager can not be cast to android.support.v4.view.ViewPagerjava.lang.ClassCastException: android.support.v4.view.ViewPager can not be cast to android.support.v4.view.ViewPager

當碰到這種問題時可以通過 instance.getClass().getClassLoader(); 得到 ClassLoader,看 ClassLoader 是否一樣。

4. 載入不同 Jar 包中公用類

現在 Host 工程包含了 common.jar, jar1.jar, jar2.jar,並且 jar1.jar 和 jar2.jar 都包含了 common.jar,我們通過 ClassLoader 將 jar1, jar2 動態載入進來,這樣在 Host 中實際是存在三份 common.jar,如: Class Diagram
我們怎麼保證 common.jar 只有一份而不會造成上面3中提到的 ClassLoader 隔離的問題呢,其實很簡單,有三種方式:
第一種:我們只要讓載入 jar1 和 jar2 的 ClassLoader 的 parent 為同一個 ClassLoader,並且該 ClassLoader 載入過 common.jar,通過上面 1 中我們知道根據雙親委託,最後都會首先被 parentClassLoader載入。
第二種:我們重寫 jar1 和 jar2 的 ClassLoader,在 loadClass 函數中我們先去某個含有 common.jar 的 ClassLoader 中 load 即可,其實就是把上面的 parentClassLoader 換掉了而已。
第三種:在產生 jar1 和 jar2 時把 common.jar 去掉,只保留 host 中一份,以 host ClassLoader 為 parentClassLoader 即可。

ClassLoader機制

Java虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校檢、轉換解析和初始化的,最終形成可以被虛擬機器直接使用的Java類型,這就是虛擬機器的類載入機制。
與那些在編譯時間進行鏈串連工作的語言不同,在Java語言裡面,類型的載入、串連和初始化都是在程式運行期間完成的,這種策略雖然會令類載入時稍微增加一些效能開銷,但是會為Java應用程式提供高度的靈活性,Java裡天生可以同代拓展的語言特性就是依賴運行期動態載入和動態連結這個特點實現的。例如,如果編寫一個面相介面的應用程式,可以等到運行時在制定實際的實作類別;使用者可以通過Java與定義的和自訂的類載入器,讓一個本地的應用程式可以在運行時從網路或其他地方載入一個二進位流作為代碼的一部分,這種組裝應用程式的方式目前已經廣泛應用於Java程式之中。從最基礎的Applet,JSP到複雜的OSGi技術,都使用了Java語言運行期類載入的特性。

Java的類載入是一個相對複雜的過程;它包括載入、驗證、準備、解析和初始化五個階段;對於開發人員來說,可控性最強的是載入階段;載入階段主要完成三件事:

根據一個類的全限定名來擷取定義此類的二進位位元組流
將這個位元組流所代表的靜態儲存結構轉化為JVM方法區中的運行時資料結構
在記憶體中產生一個代表這個類的java.lang.Class對象,作為方法區這個類的各種資料的訪問入口。
『通過一個類的全限定名擷取描述此類的二進位位元組流』這個過程被抽象出來,就是Java的類載入器模組,也即JDK中ClassLoader API。

Android Framework提供了DexClassLoader這個類,簡化了『通過一個類的全限定名擷取描述次類的二進位位元組流』這個過程;我們只需要告訴DexClassLoader一個dex檔案或者apk檔案的路徑就能完成類的載入。因此本文的內容用一句話就可以概括:

將外掛程式的dex或者apk檔案告訴『合適的』DexClassLoader,藉助它完成外掛程式類的載入

我的二維碼如下

訂閱號二維碼如下:

參考

https://gold.xitu.io/entry/578d184b79bc44005ff029b3
http://www.trinea.cn/android/java-loader-common-class/
http://www.trinea.cn/android/android-plugin/
http://weishu.me/2016/04/05/understand-plugin-framework-classloader/
http://www.infoq.com/cn/articles/ctrip-android-dynamic-loading

Android外掛程式化總結

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.