作者:johncz
當一個App釋出之後,突然發現了一個嚴重bug需要進行緊急修複,這時候公司各方就會忙得焦頭爛額:重新打包App、測試、向各個應用市場和管道換包、提示使用者更新、使用者下載下傳、覆寫安裝。有時候僅僅是為了修改了一行代碼,也要付出巨大的成本進行換包和重新釋出。
這時候就提出一個問題:有沒有辦法以更新檔的方式動态修複緊急Bug,不再需要重新釋出App,不再需要使用者重新下載下傳,覆寫安裝?
雖然Android系統并沒有提供這個技術,但是很幸運的告訴大家,答案是:可以,我們QQ空間提出了熱更新檔動态修複技術來解決以上這些問題。
空間Android獨立版5.2釋出後,收到使用者回報,結合版無法跳轉到獨立版的訪客界面,每天都較大的回報。在以前隻能緊急換包,重新釋出。成本非常高,也影響使用者的口碑。最終決定使用熱更新檔動态修複技術,向使用者下發Patch,在使用者無感覺的情況下,修複了外網問題,取得非常好的效果。
該方案基于的是android dex分包方案的,關于dex分包方案,網上有幾篇解釋了,是以這裡就不再贅述,具體可以看這裡
簡單的概括一下,就是把多個dex檔案塞入到app的classloader之中,但是android dex拆包方案中的類是沒有重複的,如果classes.dex和classes1.dex中有重複的類,當用到這個重複的類的時候,系統會選擇哪個類進行加載呢?
讓我們來看看類加載的代碼:

一個ClassLoader可以包含多個dex檔案,每個dex檔案是一個Element,多個dex檔案排列成一個有序的數組dexElements,當找類的時候,會按順序周遊dex檔案,然後從目前周遊的dex檔案中找類,如果找類則傳回,如果找不到從下一個dex檔案繼續查找。
理論上,如果在不同的dex中有相同的類存在,那麼會優先選擇排在前面的dex檔案的類,如下圖:
在此基礎上,我們構想了熱更新檔的方案,把有問題的類打包到一個dex(patch.dex)中去,然後把這個dex插入到Elements的最前面,如下圖:
好,該方案基于第二個拆分dex的方案,方案實作如果懂拆分dex的原理的話,大家應該很快就會實作該方案,如果沒有拆分dex的項目的話,可以參考一下谷歌的multidex方案實作。然後在插入數組的時候,把更新檔包插入到最前面去。
好,看似問題很簡單,輕松的搞定了,讓我們來試驗一下,修改某個類,然後打包成dex,插入到classloader,當加載類的時候出現了(本例中是QzoneActivityManager要被替換):
為什麼會出現以上問題呢?
從log的意思上來講,ModuleManager引用了QzoneActivityManager,但是發現這這兩個類所在的dex不在一起,其中:
ModuleManager在classes.dex中
QzoneActivityManager在patch.dex中
結果發生了錯誤。
這裡有個問題,拆分dex的很多類都不是在同一個dex内的,怎麼沒有問題?
讓我們搜尋一下抛出錯誤的代碼所在,嘿咻嘿咻,找到了一下代碼:
從代碼上來看,如果兩個相關聯的類在不同的dex中就會報錯,但是拆分dex沒有報錯這是為什麼,原來這個校驗的前提是:
如果引用者(也就是ModuleManager)這個類被打上了CLASS_ISPREVERIFIED标志,那麼就會進行dex的校驗。那麼這個标志是什麼時候被打上去的?讓我們在繼續搜尋一下代碼,嘿咻嘿咻~,在DexPrepare.cpp找到了一下代碼:
這段代碼是dex轉化成odex(dexopt)的代碼中的一段,我們知道當一個apk在安裝的時候,apk中的classes.dex會被虛拟機(dexopt)優化成odex檔案,然後才會拿去執行。
虛拟機在啟動的時候,會有許多的啟動參數,其中一項就是verify選項,當verify選項被打開的時候,上面doVerify變量為true,那麼就會執行dvmVerifyClass進行類的校驗,如果dvmVerifyClass校驗類成功,那麼這個類會被打上CLASS_ISPREVERIFIED的标志,那麼具體的校驗過程是什麼樣子的呢?
此代碼在DexVerify.cpp中,如下:
驗證clazz->directMethods方法,directMethods包含了以下方法:
static方法
private方法
構造函數
clazz->virtualMethods
虛函數=override方法?
概括一下就是如果以上方法中直接引用到的類(第一層級關系,不會進行遞歸搜尋)和clazz都在同一個dex中的話,那麼這個類就會被打上CLASS_ISPREVERIFIED:
是以為了實作更新檔方案,是以必須從這些方法中入手,防止類被打上CLASS_ISPREVERIFIED标志。
最終空間的方案是往所有類的構造函數裡面插入了一段代碼,代碼如下:
}`
其中AntilazyLoad類會被打包成單獨的hack.dex,這樣當安裝apk的時候,classes.dex内的類都會引用一個在不相同dex中的AntilazyLoad類,這樣就防止了類被打上CLASS_ISPREVERIFIED的标志了,隻要沒被打上這個标志的類都可以進行打更新檔操作。
然後在應用啟動的時候加載進來.AntilazyLoad類所在的dex包必須被先加載進來,不然AntilazyLoad類會被标記為不存在,即使後續加載了hack.dex包,那麼他也是不存在的,這樣螢幕就會出現茫茫多的類AntilazyLoad找不到的log。
是以Application作為應用的入口不能插入這段代碼。(因為載入hack.dex的代碼是在Application中onCreate中執行的,如果在Application的構造函數裡面插入了這段代碼,那麼就是在hack.dex加載之前就使用該類,該類一次找不到,會被永遠的打上找不到的标志)
其中:
之是以選擇構造函數是因為他不增加方法數,一個類即使沒有顯式的構造函數,也會有一個隐式的預設構造函數。
空間使用的是在位元組碼插入代碼,而不是源代碼插入,使用的是javaassist庫來進行位元組碼插入的。
隐患:
如何打包更新檔包:
空間在正式版本釋出的時候,會生成一份緩存檔案,裡面記錄了所有class檔案的md5,還有一份mapping混淆檔案。
在後續的版本中使用-applymapping選項,應用正式版本的mapping檔案,然後計算編譯完成後的class檔案的md5和正式版本進行比較,把不相同的class檔案打包成更新檔包。
備注:該方案現在也應用到我們的編譯過程當中,編譯不需要重新打包dex,隻需要把修改過的類的class檔案打包成patch dex,然後放到sdcard下,那麼就會讓改變的代碼生效。
文章來源公衆号:QQ空間終端開發團隊(qzonemobiledev)
相關推薦