首頁>技術>

概述

外掛化和熱修復,從技術實現的角度來說,原理想通。他們都是從系統載入器的角度出發,無論是採用hook方式,亦或是代理方式或者是其他底層實現,都是通過“欺騙”Android 系統的方式來讓宿主正常的載入和執行外掛(補丁)中的內容;

外掛化,更多是想把需要實現的模組或功能當做一個獨立的提取出來,減少宿主的規模,當需要使用到相應的功能時再去載入相應的模組。

熱修復,則往往是從修復bug的角度出發,強調的是在不需要二次安裝應用的前提下修復已知的bug。

宿主: 就是當前執行的APP外掛: 相對於外掛化技術來說,就是要載入執行的apk類檔案補丁: 相對於熱修復技術來說,就是要載入執行的.patch,.dex,*.apk等一系列包含dex修復內容的檔案。

了解外掛化和熱修復之前需要知道下面一些東西。

ClassLoader類載入器。每個java程式都是由class類組成的,只有把這些class類載入到JVM中,程式才能夠執行。那麼,用來載入這些類的就是ClassLoader類載入器。

關於ClassLoader和ClassLoader的雙親委託模式,可檢視之前文章。

簡述雙親委託模型

1. 當前ClassLoader首先從自己已經載入的類中查詢是否此類已經載入,如果已經載入則直接返回原來已經載入的類。

每個類載入器都有自己的載入快取,當一個類被載入了以後就會放入快取,等下次載入的時候就可以直接返回了。

2. 當前classLoader的快取中沒有找到被載入的類的時候,委託父類載入器去載入,父類載入器採用同樣的策略,首先檢視自己的快取,然後委託父類的父類去載入,一直到bootstrp ClassLoader.

3. 當所有的父類載入器都沒有載入的時候,再由當前的類載入器載入,並將其放入它自己的快取中,以便下次有載入請求的時候直接返回。

Android中有哪幾種ClassLoader?

ClassLoader直接實現的子類有BaseDexClassLoader和SecureClassLoader。

BaseDexClassLoader的子類有如下幾個:PathClassLoader、DexClassLoader、InMemoryDexClassLoader

SecureClassLoader有一個子類:

URLClassLoader

各自作用:

InMemoryDexClassLoader:載入快取中的dex檔案或檔案集。

PathClassLoader:只能載入已經安裝到Android系統中的apk檔案(/data/app目錄),是Android預設使用的類載入器。

DexClassLoader:可以載入任意目錄下的dex/jar/apk/zip檔案。

URLClassLoader:從指向JAR檔案和目錄的URL的搜尋路徑載入類和資源。

熱修復基本原理

常規的JVM類似,在Android中類的載入也是通過ClassLoader來完成。PathClassLoader和DexClassLoader都是繼承自BaseDexClassLoader,我們可以看一下BaseDexClassLoader的建構函式。

public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); }

這個建構函式只做了一件事,就是通過傳遞進來的相關引數,初始化了一個DexPathList物件。DexPathList的建構函式,就是將引數中傳遞進來的程式檔案(就是補丁檔案)封裝成Element物件,並將這些物件新增到一個Element的陣列集合dexElements中去。

對於開發者來說,我們關注的重點應該是如何去找到需要載入的類。

假設我們現在要去查詢一個名為name的class,那麼DexClassLoader將通過以下步驟實現:

在DexClassLoader的findClass 方法中通過一個DexPathList物件findClass()方法來獲取class在DexPathList的findClass 方法中,對之前構造好dexElements陣列集合進行遍歷,一旦找到類名與name相同的類時,就直接返回這個class,找不到則返回null。

總的來說,通過DexClassLoader查詢一個類,最終就是就是在一個數組中查詢特定值的操作。

綜合以上所有的觀點,我們很容易想到一種非常簡單粗暴的熱修復方案。在Application的onCreate()方法中獲取應用本身的BaseDexClassLoader,然後通過反射得到對應的dexElements建立一個新的DexClassLoader例項,然後載入sdCard上的補丁包,然後通過同樣的方法得到對應的dexElements將兩個dexElements合併,然後再利用反射將合併後的dexElements賦值給應用本身的BaseDexClassLoader

但是,Android虛擬機器和常規的JVM 不同,載入的並不是.class而是dex(準確的來說是經過優化的odex),在這樣一個過程中,勢必會有一些新的問題值得我們去關注。這個問題就是的CLASS_ISPREVERIFIED。

在apk安裝的時候系統會將dex檔案優化成odex檔案,在優化的過程中會涉及一個預校驗的過程校驗方式:假設A該類在它的static方法,private方法,建構函式,override方法中直接引用到B類。如果A類和B類在同一個dex中,那麼A類就會被打上CLASS_ISPREVERIFIED標記如果在執行時被打上CLASS_ISPREVERIFIED的類引用了其他dex的類,就會報錯而普通分包方案則不會出現這個錯誤,因為引用和被引用的兩個類一開始就不在同一個dex中,所以校驗的時候並不會被打上CLASS_ISPREVERIFIED補充一下第二條:A類如果還引用了一個C類,而C類在其他dex中,那麼A類並不會被打上標記。換句話說,只要在static方法,構造方法,private方法,override方法中直接引用了其他dex中的類,那麼這個類就不會被打上CLASS_ISPREVERIFIED標記。要在已經編譯完成後的類中植入對其他類的引用,就需要操作位元組碼,慣用的方案是插樁。常見的工具有javaassist,asm等。QQ 空間超級補丁方案

根據上面的第六條,我們只要讓所有類都引用其他dex中的某個類就可以了。

QQ空間補丁方案的關鍵就在於位元組碼的注入而不是dex的注入。它使用javaassist 插樁的方式解決了CLASS_ISPREVERIFIED的難題。

在所有類的建構函式中插入這行程式碼 System.out.println(AntilazyLoad.class); 這樣當安裝apk的時候,classes.dex內的類都會引用一個在不相同dex中的AntilazyLoad類,這樣就防止了類被打上CLASS_ISPREVERIFIED的標誌了,只要沒被打上這個標誌的類都可以進行打補丁操作。hack.dex在應用啟動的時候就要先加載出來,不然AntilazyLoad類會被標記為不存在,即使後面再載入hack.dex,AntilazyLoad類還是會提示不存在。該類只要一次找不到,那麼就會永遠被標上找不到的標記了。我們一般在Application中執行dex的注入操作,所以在Application的構造中不能加上System.out.println(AntilazyLoad.class);這行程式碼,因為此時hack.dex還沒有載入進來,AntilazyLoad並不存在。之所以選擇建構函式是因為他不增加方法數,一個類即使沒有顯式的建構函式,也會有一個隱式的預設建構函式。

插入程式碼的難點

首先在原始碼中手動插入不太可行,hack.dex此時並沒有載入進來,AntilazyLoad.class並不存在,編譯不通過。所以我們需要在原始碼編譯成位元組碼之後,在位元組碼中進行插入操作。對位元組碼進行操作的框架有很多,但是比較常用的則是ASM和javaassist。但AndroidStudio是使用Gradle構建專案,編譯-打包都是自動化的。需要對Gradle非常熟悉。Tinker

QQ空間超級補丁,“超級補丁”很多情況下意味著補丁檔案很大,而將這樣一個大資料夾載入在記憶體中構建一個Element物件,插入到陣列最前端是需要耗費時間的,無疑會印象應用啟動的速度。因此Tinker 提出了另外一種思路。

Tinker的思路是這樣的,通過修復好的class.dex 和原有的class.dex比較差生差量包補丁檔案patch.dex,在手機上這個patch.dex又會和原有的class.dex 合併生成新的檔案fix_class.dex,用這個新的fix_class.dex 整體替換原有的dexPathList的中的內容,可以說是從根本上把bug給幹掉了。

HotFix

以上提到的兩種方式,雖然策略有所不同,但總的來說都是從上層ClassLoader的角度出發,如果想要新的補丁檔案再次生效,無論你是插樁還是提前合併,都需要重新啟動應用來載入新的DexPathList。

AndFix 提供了一種執行時在Native修改Filed指標的方式,實現方法的替換,達到即時生效無需重啟,對應用無效能消耗的目的。

由於他是Native層操作,因此如果我們在Java層中新增欄位,或者是修改類的方法,他是無能為力的。

Sophix(收費)

阿里推出業界首個非侵入式熱修復方案Sophix。它提供了一套客戶端服務端一體的熱更新方案,做到了圖形介面一鍵打包、加密傳輸、簽名校驗和服務端控制釋出與灰度功能,讓你用最少的時間實現最強大可靠的全方位熱更新。

其他及總結

各個大廠還有各自的實現,比如餓了嗎的Amigo,美團的Robust,實現及優缺點各有差異,但總的來說就是兩大類

ClassLoader 載入方案Native層替換方案

綜上所述,其實對於熱修復很難有一種十分完美的解決方案。在Android開發中,四大元件使用前需要在AndroidManifest中提前宣告,而如果需要使用熱修復的方式,無論是提前佔坑亦或是動態修改,都會帶來很強的侵入性(因此,Sophix是不支援四大元件修復的,這也是其非侵入性設計理念無法避免的事情了,不知道以後會不會有新的辦法)。熱修復方案,目前最多的問題還是相容性。一句話,沒有最好的,只有合適的。

  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • grafana+prometheus+node_exporter在win10環境上的部署