前言
(廢話) 最近發現了一個問題,一些平時部落格寫的很多的程式設計師,反倒在日常的工作中,卻是業務寫的很一般,只會擺理論的人,甚至還跑出來教別人如何找工作,如何做架構,其實自己都沒搞明白。但是受眾的分層導致了輸出者的分層,教出清北學生的老師並不一定來自比這更好的學校,因此,對於部落格的輸出,一個是作為對自己學習的一個記錄,非常仔細的梳理可以非常方便的讓我們在需要的時候拿起來,再就是即使這個知識現在不用,無法深入下去,可能會遺忘的比較快,其細節可能會忘記,但是核心思想我們還是有印象的,再就是站在讀者的角度上來看,由於讀者的差異性,我們的部落格在保證無誤的前提下,一定是可以幫助到很多同學的,本著這些原則,一週一篇的輸出,希望可以堅持下去。
(正題) 最近在做熱修復的相關調研,接著部落格的專題,可以好好的發一波文章了,今天要分析的是ClassLoader方案最簡單的一個Nvwa,本篇文章將會從class查詢過程到Nvwa的實現,以及在實現的時候解決了什麼問題,這幾個方面展開,逐步講解。
基礎使用
初始化
Nuwa.init(this);
複製程式碼
裝載補丁包
Nuwa.loadPatch(this,patchFile)
複製程式碼
原始碼分析
類的查詢
對於類的載入,在通過DexClassLoader進行載入的時候,通過DexPathList進行載入,其中維護了一個Element的陣列,在查詢的類的時候,會遍歷陣列查詢類,如果找到則返回。對於陣列遍歷查詢的程式碼如下所示。
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
複製程式碼
- Nvwa的初始化
public static void init(Context context) {
File dexDir = new File(context.getFilesDir(), DEX_DIR);
dexDir.mkdir();
String dexPath = null;
try {
//拷貝Asset目錄下的Hack.apk到指定路徑
dexPath = AssetUtils.copyAsset(context, HACK_DEX, dexDir);
} catch (IOException e) {
e.printStackTrace();
}
//從拷貝後的指定路徑載入apk
loadPatch(context, dexPath);
}
複製程式碼
在nvwaw的init方法中進行的操作是將asset中的一個hack.apk拷貝出來,然後將其作為補丁進行裝載。
- 補丁的載入
public static void loadPatch(Context context, String dexPath) {
if (context == null) {
return;
}
if (!new File(dexPath).exists()) {
return;
}
File dexOptDir = new File(context.getFilesDir(), DEX_OPT_DIR);
dexOptDir.mkdir();
try {
DexUtils.injectDexAtFirst(dexPath, dexOptDir.getAbsolutePath());
} catch (Exception e) {
e.printStackTrace();
}
}
複製程式碼
由上面程式碼,可以看出核心的實現在對DexUtils
的injectDexAtFirst
呼叫上。
public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
Object newDexElements = getDexElements(getPathList(dexClassLoader));
Object allDexElements = combineArray(newDexElements, baseDexElements);
Object pathList = getPathList(getPathClassLoader());
ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);
}
複製程式碼
將兩個Dex進行合併,將補丁dex塞在陣列的前面,然後通過反射的方式設定進去,通過這種方式,根據上面的類載入邏輯,可以知道,對於類的載入是從陣列的最開始的位置進行查詢載入的,當前面的dex查詢到相應的類之後,就會停止後面的查詢,這樣,我們通過補丁的替換的類就會生效。
private static Object combineArray(Object firstArray, Object secondArray) {
Class<?> localClass = firstArray.getClass().getComponentType();
int firstArrayLength = Array.getLength(firstArray);
int allLength = firstArrayLength + Array.getLength(secondArray);
Object result = Array.newInstance(localClass, allLength);
for (int k = 0; k < allLength; ++k) {
if (k < firstArrayLength) {
Array.set(result, k, Array.get(firstArray, k));
} else {
Array.set(result, k, Array.get(secondArray, k - firstArrayLength));
}
}
return result;
}
複製程式碼
存在問題
通過上述的方式,我們將差異補丁單獨打一個包,然後進行下發,從而使得我們的類得到修復。但是在這樣實現的時候,存在一個問題,就是 在Dalvik 虛擬機器對於dex的一個優化。Dalvik 虛擬機器在啟動的時候,會有許多的啟動引數,其中有一項就是verify,當verify被開啟的時候,doVerify變數為true,則進行類的校驗(dvmVerifyClass方法呼叫)。若校驗成功,則這個類會被打上標記:CLASS_ISPREVERIFIED。
這麼做,是防止外部DEX注入的一個安全方案,即保證執行期的Class與其直接引用類之間所在的DEX關係要與安裝時候一致,也是為了防止類被篡改校驗類的合法性。Dalvik 虛擬機器在安裝期間,為Class 打上 CLASS_ISPREVERIFIED 是為了提高效能,下次使用時,則會省去校驗操作,提高訪問效率。dvm在執行期載入Class時候,會對其記憶體中對應的直接引用類進行校驗,如果該類存在與直接引用類所在的dex不是同一個,則直接報“pre-verification” 錯誤,該類無法載入。
由於這一個限制,導致我們的補丁包無法在被呼叫到的時候,就會丟擲異常,因此我們需要讓我們的補丁包,如何通過這次校驗, 不被打上CLASS_ISPREVERIFIED,這樣,我們的補丁包在被載入的時候,就不會丟擲異常了。
nvwa採取的方式就是插樁的方式,在每一個類裡去引用到另一個獨立dex中的類,也會是在初始化的時候載入的hack.apk中的Hack.class,通過這種方式,可以讓我們的類不會被打上這個標籤。這樣就可以繼續裝載其它Dex中的類。
插樁存在一個什麼問題呢?由於沒有打上驗證標籤,導致每個類的裝載的時候都進行驗證。
微信在對插裝和不插樁做的測試中。在連續載入700個50行的類,還有統計應用啟動耗時得到的資料,700個類:不插樁:84ms,插樁:685ms。啟動耗時:4934ms,7240ms。
結語
每週一更,由於最近業務需求較多,更新速度明顯慢了很多了,因此本篇分析的也是一很簡單的框架。接下來,將會逐步深入,分析一些更為複雜的熱修復方案框架。