一、什么是熱更新
簡單來講熱更新就是線上版本出了大問題,不用重新發(fā)版本,只需要打個補丁包,客戶端進行下載整頓即可修復那個問題。
二、熱修復與插件化
1.插件化的內容是原來的app中沒有的東西,而熱更新是將原有的東西做了改動
2.插件化在代碼中有固定的入口,而熱更新則可能改變任何一個位置的代碼
三、熱更新原理
1.虛擬機在加載class時會通過雙親委托機制去加載一個類,先從自己的緩存中看看能不能找到該class,不能找到則先讓父類去加載,如果父類找不到再讓子類去加載。
2.通過查看ClassLoader源碼,findClass方法是由每個子類自己實現(xiàn)的,比如 BootClassLoader或者BaseDexClassLoader。而PathClassLoader和DexClassLoader是繼承自BaseDexClassLoader的,它的findClass也是在BaseDexClassLoader里面實現(xiàn)的。在安卓中,我們平時在使用時遇到的那些類,基本都是由PathClassLoader來進行加載的。
3.BaseDexClassLoader的findClass里面使用了另一個對象DexPathList去查找對應的class,這是安卓里面特有的實現(xiàn)。在DexPathList對象里面有一個屬性dexElements,dexElements是用于存放加載好了的dex數(shù)組的,查找class是從這個數(shù)組里面從前往后找的
4.dexElements里面存放的是Element對象,findClass最終會交給Element去實現(xiàn),Element又會交給Element里面的一個屬性DexFile去實現(xiàn)。最終是交給native方法來實現(xiàn)的
5.回到上面的DexPathList對象從dexElements數(shù)組里面查找class,從數(shù)組的前面往后面找,找到了就返回結果,不再繼續(xù)查找
6.我們修復好了bug的時候,把那些有改動的java源碼編譯成class,再打包成dex,然后通過反射技術放到dexElements數(shù)組的最前面,這樣系統(tǒng)在通過PathClassLoader找class時,首先找到的是我們放在最前面的修復好了的class,然后就不會再往后面找了,相當于實現(xiàn)了熱修復。這樣有bug的class就不會被用到了。
7.其中上面一步的反射流程:獲取到PathClassLoader,然后反射獲取父類中的DexPathList對象,然后再反射到DexPathList對象中的dexElements數(shù)組。然后將補丁dex轉為Element對象,插入到dexElements數(shù)組的最前面(先復制出來,再合并,再通過反射放回去)
一句話小結:將修復后的類通過反射技術放到dexElements的最前面,從而讓系統(tǒng)在加載類的時候優(yōu)先加載到已修復的class,從而達到熱修復
四、熱更新代碼實現(xiàn)
下面是核心代碼
fun loadPatch() {
val appContext = App.getAppContext()
//HotFixUtil::class.java.classLoader 即 PathClassLoader,默認的類加載器
//1. PathClassLoader->BaseDexClassLoader 拿到當前程序的dexElements數(shù)組
val oldPathList = FieldUtil.getFieldValue(BaseDexClassLoader::class.java, HotFixUtil::class.java.classLoader, "pathList")
val DexPathListClazz = Class.forName("dalvik.system.DexPathList")
val oldDexElements = FieldUtil.getFieldValue(DexPathListClazz, oldPathList, "dexElements")
//2. 補丁包 構建一個DexClassLoader 用來加載補丁包里面的class
//我這里是模擬從網上下載的過程,我直接將補丁放assets目錄下了
val inputStream = appContext.assets.open("hotfix.dex")
val file = File("${appContext.cacheDir}/hotfix.dex")
val outputStream = FileOutputStream(file)
inputStream.copyTo(outputStream)
val dexClassLoader = DexClassLoader(file.path, appContext.cacheDir.path, null, HotFixUtil::class.java.classLoader)
val newPathList = FieldUtil.getFieldValue(BaseDexClassLoader::class.java, dexClassLoader, "pathList")
val newDexElements = FieldUtil.getFieldValue(DexPathListClazz, newPathList, "dexElements")
//3. 合并2個數(shù)組
val combineArray = InsertDexUtils.combineArray(newDexElements, oldDexElements)
//4. 將合并好的數(shù)組放進當前程序的BaseDexClassLoader的dexElements數(shù)組中,這樣在找class時就先找到補丁里面的class,熱修復成功
FieldUtil.setField(DexPathListClazz, oldPathList!!, "dexElements", combineArray)
}
五、熱更番外篇
除了我們常見讓系統(tǒng)優(yōu)先加載修改后class文件,美團熱更框架robust,另辟蹊徑,使用字節(jié)碼插磚方法,
- 給每個class插入一個public static的ChangeQuickRedirect對象,
- 當該方法的changeQuickRedirect不為空時,直接將參數(shù)直接傳入PatchProxy的accessDispatchVoid/accessDispatch方法并返回,這樣做跳過了原方法后面的代碼,執(zhí)行修改后的方法,達到熱更作用