(廢話)
最近發現了一個問題,一些平時博客寫的不少的程序員,反倒在平常的工做中,倒是業務寫的很通常,只會擺理論的人,甚至還跑出來教別人如何找工做,如何作架構,其實本身都沒搞明白。可是受衆的分層致使了輸出者的分層,教出清北學生的老師並不必定來自比這更好的學校,所以,對於博客的輸出,一個是做爲對本身學習的一個記錄,很是仔細的梳理能夠很是方便的讓咱們在須要的時候拿起來,再就是即便這個知識如今不用,沒法深刻下去,可能會遺忘的比較快,其細節可能會忘記,可是核心思想咱們仍是有印象的,再就是站在讀者的角度上來看,因爲讀者的差別性,咱們的博客在保證無誤的前提下,必定是能夠幫助到不少同窗的,本着這些原則,一週一篇的輸出,但願能夠堅持下去。git
(正題)
最近在作熱修復的相關調研,接着博客的專題,能夠好好的發一波文章了,今天要分析的是ClassLoader方案最簡單的一個Nvwa,本篇文章將會從class查找過程到Nvwa的實現,以及在實現的時候解決了什麼問題,這幾個方面展開,逐步講解。程序員
初始化github
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; }
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。
每週一更,因爲最近業務需求較多,更新速度明顯慢了不少了,所以本篇分析的也是一很簡單的框架。接下來,將會逐步深刻,分析一些更爲複雜的熱修復方案框架。