當一個App發佈以後,忽然發現了一個嚴重bug須要進行緊急修復,這時候公司各方就會忙得焦頭爛額:從新打包App、測試、向各個應用市場和渠道換包、提示用戶升級、用戶下載、覆蓋安裝。有時候僅僅是爲了修改了一行代碼,也要付出巨大的成本進行換包和從新發布。
這時候就提出一個問題:有沒有辦法以補丁的方式動態修復緊急Bug,再也不須要從新發布App,再也不須要用戶從新下載,覆蓋安裝?
雖然Android系統並無提供這個技術,可是很幸運的告訴你們,答案是:能夠,咱們QQ空間提出了熱補丁動態修復技術來解決以上這些問題。html
空間Android獨立版5.2發佈後,收到用戶反饋,結合版沒法跳轉到獨立版的訪客界面,天天都較大的反饋。在之前只能緊急換包,從新發布。成本很是高,也影響用戶的口碑。最終決定使用熱補丁動態修復技術,向用戶下發Patch,在用戶無感知的狀況下,修復了外網問題,取得很是好的效果。java
該方案基於的是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不在一塊兒,其中:android
讓咱們搜索一下拋出錯誤的代碼所在,嘿咻嘿咻,找到了一下代碼:
從代碼上來看,若是兩個相關聯的類在不一樣的dex中就會報錯,可是拆分dex沒有報錯這是爲何,原來這個校驗的前提是:
api
若是引用者(也就是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都在同一個dex中的話,那麼這個類就會被打上CLASS_ISPREVERIFIED:
因此爲了實現補丁方案,因此必須從這些方法中入手,防止類被打上CLASS_ISPREVERIFIED標誌。
最終空間的方案是往全部類的構造函數裏面插入了一段代碼,代碼以下:if (ClassVerifier.PREVENT_VERIFY) { System.out.println(AntilazyLoad.class); }
緩存
其中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_ISPREVERIFIED標誌是爲了提升性能的,咱們強制防止類被打上標誌是否會影響性能?這裏咱們會作一下更加詳細的性能測試.可是在大項目中拆分dex的問題已經比較嚴重,不少類都沒有被打上這個標誌。
如何打包補丁包:安全
App的上線發佈是咱們程序猿開心的事情,證實着一段時間來成果的進步和展示。可是隨着App的上線手機App市場,接下來的更新維護工做便成了」屢見不鮮「。尤爲是在創業公司,隨着業務等不穩定性因素,前期App的更新工做更爲頻繁,可能兩天一小改,三天一大改的狀況常常發生。微信
那麼應對版本更新的同時,須要咱們不斷將新版本上線,並下發到用戶,此時兩個典型的問題發生了:cookie
(1)發版的週期過長app
(2)用戶的App版本更新進度緩慢
因此,在傳統App的開發模式下,須要一種手段來改變當前存在的問題。若是存在一種方案能夠在不發版的前提下也能夠修復線上App的Bug,那麼以上兩個問題就都得以解決。此時一系列的第三方庫撲面而來,阿里的AndFix、騰訊的Qzone修復、以及近期開源的微信Tinker應運而生。
關於各類熱更新庫的使用,網上已經有不少的博文來介紹。本篇博客着重和你們分享一下關於QQ空間熱更新的原理解析。
關於原理的分析,大體分爲以下模塊:
(1)熱修復機制的產生
(2)Android類加載機制
一、熱修復機制的產生
隨着App業務不斷疊加,以及第三方庫的多種依賴,相信不少人某天運行程序忽然出現以下異常:
1 java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536 2 at com.android.dx.merge.DexMerger$6.updateIndex(DexMerger.java:501) 3 at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:282) 4 at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:490) 5 at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:167) 6 at com.android.dx.merge.DexMerger.merge(DexMerger.java:188) 7 at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:439) 8 at com.android.dx.command.dexer.Main.runMonoDex(Main.java:287) 9 at com.android.dx.command.dexer.Main.run(Main.java:230) 10 at com.android.dx.command.dexer.Main.main(Main.java:199) 11 at com.android.dx.command.Main.main(Main.java:103)
從異常信息中,咱們不難發現:method ID not in 65536。而且大體能夠看出是在dex層跑出的異常。什麼意思呢?
咱們編寫的Java業務代碼爲.java類型文件,當咱們編譯運行一個完整的App項目時,系統會執行以下流程:
.Java --> .class --> dex --> (odex ) --> Apk
當class文件被打包成一個dex文件時,因爲dex文件的限制,方法的ID爲short型,因此一個dex文件存放的方法數最多爲65536。超過了該數,系統就會拋出上面所述的異常。爲了解決這個問題,Google爲咱們提供了multidex解決方案,即把dex文件分爲主、副兩類。系統只加載第一個dex文件(主dex),剩餘的dex文件(副dex)在Application中的onCreate方法中,以資源的方式被加載到系統的ClassLoader。能夠理解爲:一個APK能夠包含多個dex文件。這樣就解決了65536問題的同時。也爲熱修復提供了實現方案:將修復後的dex文件下發到客戶端(App),客戶端在啓動後就會加載最新的dex文件。
關於如何實現加載最新dex文件,咱們還須要瞭解下Android Davilk虛擬機的類加載流程。
二、Android類加載機制
Android虛擬機對於類的加載機制爲:同一個類只會加載一次。因此要實現熱修復的前提就是:讓下發到客戶端的補丁包類要比以前存在bug的類優先加載。相似於一種「替換」的解決。如何實現優先加載呢?咱們先來了解下Davilk虛擬機的類加載方式。
Java虛擬機JVM的類加載是:ClassLoader。一樣Android系統提供了兩種類加載方式:
(1)DexClassLoader
(2)PathClassLoader
首先從源碼中深刻:
libcore/dalvik/src/main/java/dalvik/system/
(1)DexClassLoader源碼:
package dalvik.system; 19import java.io.File; 36public class DexClassLoader extends BaseDexClassLoader { 37 /** 38 * Creates a {@code DexClassLoader} that finds interpreted and native 39 * code. Interpreted classes are found in a set of DEX files contained 40 * in Jar or APK files. 41 * 42 * <p>The path lists are separated using the character specified by the 43 * {@code path.separator} system property, which defaults to {@code :}. 44 * 45 * @param dexPath the list of jar/apk files containing classes and 46 * resources, delimited by {@code File.pathSeparator}, which 47 * defaults to {@code ":"} on Android 48 * @param optimizedDirectory directory where optimized dex files 49 * should be written; must not be {@code null} 50 * @param libraryPath the list of directories containing native 51 * libraries, delimited by {@code File.pathSeparator}; may be 52 * {@code null} 53 * @param parent the parent class loader 54 */ 55 public DexClassLoader(String dexPath, String optimizedDirectory, 56 String libraryPath, ClassLoader parent) { 57 super(dexPath, new File(optimizedDirectory), libraryPath, parent); 58 } 59}
(2)PathClassLoader:
25 public class PathClassLoader extends BaseDexClassLoader { 26 /** 27 * Creates a {@code PathClassLoader} that operates on a given list of files 28 * and directories. This method is equivalent to calling 29 * {@link #PathClassLoader(String, String, ClassLoader)} with a 30 * {@code null} value for the second argument (see description there). 31 * 32 * @param dexPath the list of jar/apk files containing classes and 33 * resources, delimited by {@code File.pathSeparator}, which 34 * defaults to {@code ":"} on Android 35 * @param parent the parent class loader 36 */ 37 public PathClassLoader(String dexPath, ClassLoader parent) { 38 super(dexPath, null, null, parent); 39 } 40 41 /** 42 * Creates a {@code PathClassLoader} that operates on two given 43 * lists of files and directories. The entries of the first list 44 * should be one of the following: 45 * 46 * <ul> 47 * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as 48 * well as arbitrary resources. 49 * <li>Raw ".dex" files (not inside a zip file). 50 * </ul> 51 * 52 * The entries of the second list should be directories containing 53 * native library files. 54 * 55 * @param dexPath the list of jar/apk files containing classes and 56 * resources, delimited by {@code File.pathSeparator}, which 57 * defaults to {@code ":"} on Android 58 * @param libraryPath the list of directories containing native 59 * libraries, delimited by {@code File.pathSeparator}; may be 60 * {@code null} 61 * @param parent the parent class loader 62 */ 63 public PathClassLoader(String dexPath, String libraryPath, 64 ClassLoader parent) { 65 super(dexPath, null, libraryPath, parent); 66 } 67}
從源碼能夠看出,DexClassLoader和PathClassLoaderr繼承自BaseDexClassLoader。
(1)PathClassLoader能夠操做本地文件系統的文件列表或目錄中的classes。PathClassLoader負責加載系統類和主Dex中的類。
(2)DexClassLoader是一個能夠從包含classes.dex實體的.jar或.apk文件中加載classes的類加載器。DexClassLoader負責加載其餘dex文件(副dex)中的類。
既然是類加載器,必然存在類加載方法,繼續查看源碼,能夠發現BaseDexClassLoader提供了findClass方法用於加載類:
(1)BaseDexClassLoader源碼:
17 package dalvik.system; 18 19 import java.io.File; 20 import java.net.URL; 21 import java.util.ArrayList; 22 import java.util.Enumeration; 23 import java.util.List; 29 public class BaseDexClassLoader extends ClassLoader { 30 private final DexPathList pathList; 45 public BaseDexClassLoader(String dexPath, File optimizedDirectory, 46 String libraryPath, ClassLoader parent) { 47 super(parent); 48 this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory); 49 } 50 51 @Override 52 protected Class<?> findClass(String name) throws ClassNotFoundException { 53 List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); 54 Class c = pathList.findClass(name, suppressedExceptions); 55 if (c == null) { 56 ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList); 57 for (Throwable t : suppressedExceptions) { 58 cnfe.addSuppressed(t); 59 } 60 throw cnfe; 61 } 62 return c; 63 }
在findClass方法中,又調用了DexPathList對象的findClass方法,DexPathList源碼以下:
17 package dalvik.system; 18 19 import java.io.File; 20 import java.io.IOException; 21 import java.net.MalformedURLException; 22 import java.net.URL; 23 import java.util.ArrayList; 24 import java.util.Arrays; 25 import java.util.Collections; 26 import java.util.Enumeration; 27 import java.util.List; 28 import java.util.zip.ZipFile; 29 import libcore.io.ErrnoException; 30 import libcore.io.IoUtils; 31 import libcore.io.Libcore; 32 import libcore.io.StructStat; 33 import static libcore.io.OsConstants.*; 48/*package*/ final class DexPathList { 49 private static final String DEX_SUFFIX = ".dex"; 50 private static final String JAR_SUFFIX = ".jar"; 51 private static final String ZIP_SUFFIX = ".zip"; 52 private static final String APK_SUFFIX = ".apk"; 53 54 /** class definition context */ 55 private final ClassLoader definingContext; 56 57 /** 58 * List of dex/resource (class path) elements. 59 * Should be called pathElements, but the Facebook app uses reflection 60 * to modify 'dexElements' (http://b/7726934). 61 */ 62 private final Element[] dexElements; 63 64 /** List of native library directories. */ 65 private final File[] nativeLibraryDirectories; 305 /** 306 * Finds the named class in one of the dex files pointed at by 307 * this instance. This will find the one in the earliest listed 308 * path element. If the class is found but has not yet been 309 * defined, then this method will define it in the defining 310 * context that this instance was constructed with. 311 * 312 * @param name of class to find 313 * @param suppressed exceptions encountered whilst finding the class 314 * @return the named class or {@code null} if the class is not 315 * found in any of the dex files 316 */ 317 public Class findClass(String name, List<Throwable> suppressed) { 318 for (Element element : dexElements) { 319 DexFile dex = element.dexFile; 320 321 if (dex != null) { 322 Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); 323 if (clazz != null) { 324 return clazz; 325 } 326 } 327 } 328 if (dexElementsSuppressedExceptions != null) { 329 suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); 330 } 331 return null; 332 }
package dalvik.system; 19 import java.io.File; 20 import java.io.FileNotFoundException; 21 import java.io.IOException; 22 import java.util.ArrayList; 23 import java.util.Enumeration; 24 import java.util.List; 25 import libcore.io.ErrnoException; 26 import libcore.io.Libcore; 27 import libcore.io.StructStat; 28 29/** 30 * Manipulates DEX files. The class is similar in principle to 31 * {@link java.util.zip.ZipFile}. It is used primarily by class loaders. 32 * <p> 33 * Note we don't directly open and read the DEX file here. They're memory-mapped 34 * read-only by the VM. 35 */ 36 public final class DexFile { 37 private int mCookie; 38 private final String mFileName; 39 private final CloseGuard guard = CloseGuard.get(); 207 /** 208 * See {@link #loadClass(String, ClassLoader)}. 209 * 210 * This takes a "binary" class name to better match ClassLoader semantics. 211 * 212 * @hide 213 */ 214 public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) { 215 return defineClass(name, loader, mCookie, suppressed); 216 } 218 private static Class defineClass(String name, ClassLoader loader, int cookie, 219 List<Throwable> suppressed) { 220 Class result = null; 221 try { 222 result = defineClassNative(name, loader, cookie); 223 } catch (NoClassDefFoundError e) { 224 if (suppressed != null) { 225 suppressed.add(e); 226 } 227 } catch (ClassNotFoundException e) { 228 if (suppressed != null) { 229 suppressed.add(e); 230 } 231 } 232 return result;
DexFile類中,loadClassBinaryName方法中調用了defineClass方法,該方法直接經過defineClassNative執行Android原生層代碼...到此爲止,整個加載流程就走完了。
因此,要實現熱修復的就必需要在DexPathList中遍歷Element元素時,讓補丁dex在Element數組中的爲止優先於原有已存在的dex。這樣,當系統遍歷dexElement時,就能夠加載最新補丁dex,實現dex的 「替換」。1 /** 2 * Created by Song on 2017/5/15. 3 */ 4 public class Cal { 5 public float calculate() { 6 // 很明顯,會有算數異常拋出 7 return 1 / 0; 8 } 9 }
1 public class MainActivity extends AppCompatActivity { 2 3 private Cal cl; 4 @Override 5 protected void onCreate(Bundle savedInstanceState) { 6 super.onCreate(savedInstanceState); 7 setContentView(R.layout.activity_main); 8 cl = new Cal(); 9 } 10 11 /** 12 * 點擊按鈕測試 13 * @param view 14 */ 15 public void cal(View view) { 16 cl.calculate(); 17 }
1 /** 2 * Created by Song on 2017/5/15. 3 */ 4 public class Cal { 5 public float calculate() { 6 // 修改後的,不存在任何問題 7 return 1 / 1; 8 } 9 }
補丁包實際上是一個dex文件。dex文件的造成過程爲:.class --> jar --> dex。因此,先要將class文件打包爲jar。
從新編譯後的class文件在app / build / intermediates / classes / debug / 包名 / ...1 /** 2 * Created by Song on 2017/5/15. 3 */ 4 public class MainApplication extends Application { 5 6 @Override 7 public void onCreate() { 8 super.onCreate(); 9 // 獲取補丁 執行注入 10 11 String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch_dex.jar"); 12 File file = new File(dexPath); 13 if (file.exists()) { 14 Log.e("-----","開始................."); 15 inject(dexPath); 16 } 17 } 18 19 /** 20 * 要注入的dex的路徑 21 */ 22 private void inject(String path) { 23 try { 24 // 獲取classes的dexElements 25 Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader"); 26 Object pathList = getField(cl, "pathList", getClassLoader()); 27 Object baseElements = getField(pathList.getClass(), "dexElements", pathList); 28 // 獲取patch_dex的dexElements(須要先加載dex) 29 String dexopt = getDir("dexopt", 0).getAbsolutePath(); 30 DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader()); 31 Object obj = getField(cl, "pathList", dexClassLoader); 32 Object dexElements = getField(obj.getClass(), "dexElements", obj); 33 // 合併兩個Elements 34 Object combineElements = combineArray(dexElements, baseElements); 35 // 將合併後的Element數組從新賦值給app的classLoader 36 setField(pathList.getClass(), "dexElements", pathList, combineElements); 37 } catch (ClassNotFoundException e) { 38 e.printStackTrace(); 39 } catch (IllegalAccessException e) { 40 e.printStackTrace(); 41 } catch (NoSuchFieldException e) { 42 e.printStackTrace(); 43 } 44 } 45 46 /** 47 * 經過反射獲取對象的屬性值 48 */ 49 private Object getField(Class<?> cl, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException { 50 Field field = cl.getDeclaredField(fieldName); 51 field.setAccessible(true); 52 return field.get(object); 53 } 54 55 /** 56 * 經過反射設置對象的屬性值 57 */ 58 private void setField(Class<?> cl, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException { 59 Field field = cl.getDeclaredField(fieldName); 60 field.setAccessible(true); 61 field.set(object, value); 62 } 63 64 /** 65 * 經過反射合併兩個數組 66 */ 67 private Object combineArray(Object firstArr, Object secondArr) { 68 int firstLength = Array.getLength(firstArr); 69 int secondLength = Array.getLength(secondArr); 70 int length = firstLength + secondLength; 71 72 Class<?> componentType = firstArr.getClass().getComponentType(); 73 Object newArr = Array.newInstance(componentType, length); 74 for (int i = 0; i < length; i++) { 75 if (i < firstLength) { 76 Array.set(newArr, i, Array.get(firstArr, i)); 77 } else { 78 Array.set(newArr, i, Array.get(secondArr, i - firstLength)); 79 } 80 } 81 return newArr; 82 } 83 }
分析:
3、更詳細介紹:
Qzone 超級補丁技術基於dex分包方案,使用了多dex加載(multidex)的原理,大體的過程就是:把BUG方法修復之後,放到一個單獨的dex文件,而後插入到dexElements數組的最前面,讓虛擬機去加載修復完後的方法。
該方案的靈感來源?
沒錯就是類加載機制,相信大部分同窗都對它有所瞭解吧。
Android應用程序本質上使用的是java開發,使用標準的java編譯器編譯出Class文件,和普通的java開發不一樣的地方是把class文件再從新打包成dex類型的文件,這種從新打包會對Class文件內部的各類函數表、變量表等進行優化,最終產生了odex文件。odex文件是一種通過android打包工具優化後的Class文件,所以加載這樣特殊的Class文件就須要特殊的類裝載器,因此android中提供了DexClassLoader類。
類圖:
Android使用的是Dalvik虛擬機裝載class文件,因此classloader不一樣於java默認類庫rt.jar包中java.lang.ClassLoader, 能夠看到android中的classloader作了些修改,可是原理仍是差很少的。
學過java的同窗都知道, 類加載器是採用雙親委派機制來進行類加載的。
某個特定的類加載器在接到加載類的請求時,首先將加載任務委託給父類加載器,依次遞歸,若是父類加載器能夠完成類加載任務,就成功返回;只有父類加載器沒法完成此加載任務時,才本身去加載。
雙親委派機制 從ClassLoader.java 源代碼能夠清晰的看出來:
ClassLoader.java
流程大概以下:
1.判斷類是否已經加載過;
2.父類加載器優先加載;
3.parent爲null,則調用BootstrapClassLoader進行加載 ;
4.若是class依舊沒有找到,則調用當前類加載器的findClass方法進行加載;
BaseDexClassLoader.java
DexPathList.java
DexFile.java(\dalvik\dx\src\com\android\dx\dex\file\DexFile.java)
defineClassNative(android4.4版本,區分ART 和Dalvik兩種狀況)
1.ART 環境 [art\runtime\native\dalvik_system_DexFile.cc]
2.Dalvik 環境 [\dalvik\vm\native\dalvik_system_DexFile.cpp ]
(注:dvmDefineClass函數則是類加載機制中最爲核心的邏輯,因爲和本文深刻探索的方向關聯性不強,就不做深究了。源碼在 dalvik2/vm/oo/Class.cpp中,有興趣可自行研究。)
從以上類加載機制的源碼中咱們能夠分析出,當找類的時候,會按順序遍歷dex文件,而後從當前遍歷的dex文件中找類,若是找類則返回,若是找不到繼續從下一個dex文件查找。理論上,若是在不一樣的dex中有相同的類存在,那麼會優先選擇排在前面的dex文件的類,Qzone方案的靈感就是從上述的DexPathList類中的for循環體而來。
在此基礎上,Qzone 團隊構想了熱補丁的方案,把有問題的類打包到一個dex(patch.dex)中去,而後把這個dex插入到Elements的最前面,以下圖:
若是懂拆分dex的原理的話,你們應該很快就會實現該方案。若是沒有拆分dex的項目的話,能夠參考一下谷歌的multidex方案實現,而後在插入數組的時候,把補丁包插入到最前面去。
當patch.dex中包含Main.class時就會優先加載,在後續的DEX中遇到Main.class的話就會直接返回而不去加載,這樣就達到了修復的目的。看似問題很簡單,輕鬆的搞定了,Qzone一開始按照以上思路進行了實踐,但在實際操做中,出現了 unexpected DEX 的異常。這個問題是由於在Dalvik環境中,類被打上CLASS_ISPREVERIFIED的標誌,主動拋出異常報錯。
爲何系統要給類打上CLASS_ISPREVERIFIED的標誌?
咱們知道,在APK安裝時,虛擬機須要將classes.dex優化成odex文件,而後纔會執行。在這個過程當中,會進行類的verify操做,爲了提升運行性能,若是調用關係的類都在同一個DEX中的話,就會被打上CLASS_ISPREVERIFIED的標誌,而後寫入odex文件,代表它沒有被其餘Dex的類引用。
規避 Dalvik 下 「unexpected DEX」 的異常。
以下是 dalvik 的一段源碼,當補丁安裝後,首次使用到補丁裏的類時會調用到這裏, 源代碼以下:
[dalvik/vm/oo/Resolve.cpp]
從代碼邏輯咱們能夠看出,須要同時知足代碼中標出來的三個條件,纔會出現異常,這三個條件的含義以下:
所以,想要避免補丁類加載時發生 「unexpected DEX 」 的異常,則須要從以上三個地方來入手。
Qzone 的超級補丁方案採用的是經過繞過這裏的第二個判斷來避免報錯的。若是一個類被打上了CLASS_ISPREVERIFIED標誌,那麼就會進行dex的校驗。而要避免報錯,首先得弄清楚它是什麼條件下才會被打上。繼續搜索源代碼,發如今DexPrepare.cpp找到了以下代碼:
[dalvik\vm\analysis\DexPrepare.cpp]
這段代碼是dex轉化成odex(dexopt)的代碼中的一段,咱們知道當一個apk在安裝的時候,apk中的classes.dex會被虛擬機(dexopt)優化成odex文件,而後纔會拿去執行。
虛擬機在啓動的時候,會有許多的啓動參數,其中一項就是verify選項,當verify選項被打開的時候,上面doVerify變量爲true,那麼就會執行dvmVerifyClass進行類的校驗,若是dvmVerifyClass函數校驗類成功,那麼這個類會被打上CLASS_ISPREVERIFIED的標誌。
DexClassDef 結構體代碼:
struct DexClassDef {
u4 classIdx; //類的類型, DexTypeId中的索引下標
u4 accessFlags; //類的訪問標誌
u4 superclassIdx; //父類類型, DexTypeId中的索引下標
u4 interfacesOff; //接口偏移, 指向DexTypeList的結構
u4 sourceFileIdx; //源文件名, DexStringId中的索引下標
u4 annotationsOff; //註解偏移, 指向DexAnnotationsDirectoryItem的結構
u4 classDataOff; //類數據偏移, 指向DexClassData的結構
u4 staticValuesOff; //類靜態數據偏移, 指向DexEncodedArray的結構
};
而具體的校驗過程,即dvmVerifyClass函數是什麼樣子的呢?咱們繼續往下探索。
代碼在DexVerify.cpp中,以下:
[dalvik\vm\analysis\DexVerify.cpp]
該方法作了三件事情:
1. 是否已被校驗過?
2. 驗證clazz->directMethods方法,directMethods包含了如下方法:
● static方法
● private方法
● 構造方法
3. clazz->virtualMethods。虛函數=override方法?
歸納一下就是,只要在static方法,private方法,構造方法,override方法中直接引用了其餘dex中的類,那麼這個類就不會被打上CLASS_ISPREVERIFIED標記。也就是說若是以上方法中直接引用到的類(第一層級關係,不會進行遞歸搜索)和clazz都在同一個dex中的話,那麼這個類就會被打上CLASS_ISPREVERIFIED。
搞清了前因後果,因此就能夠從這些地方入手。最終Qzone的方案是往全部補丁類的構造函數裏面插入了一段代碼,來引用另一個dex的類,防止類被打上CLASS_ISPREVERIFIED標誌。代碼以下:
if (ClassVerifier.PREVENT_VERIFY) { System.out.println(AntilazyLoad.class); }
打補丁包:
1.在正式版本發佈的時候,會生成一份緩存文件,裏面記錄了全部class文件的md5,還有一份mapping混淆文件。
2. 在後續的版本中使用-applymapping選項,應用正式版本的mapping文件,而後計算編譯完成後的class文件的md5和正式版本進行比較,把不相同的class文件打包成補丁包。
Hook及加載patch操做:
1. 打包過程當中,會往全部補丁類的構造函數裏面插一段代碼。
2. 其中AntilazyLoad類會被打包成單獨的hack.dex,當安裝apk的時候,patch.dex內的類都會引用一個在不相同dex中的AntilazyLoad類,這樣就防止了類被打上CLASS_ISPREVERIFIED的標誌,只要沒被打上這個標誌的類均可以進行打補丁操做。
3. 先加載進來AntilazyLoad類,否則AntilazyLoad類會被標記爲不存在,即便後續加載了hack.dex包也於事無補。
4. 獲取到當前應用的Classloader,即爲BaseDexClassloader。
5. 經過反射獲取到它的DexPathList屬性對象pathList。
6. 經過反射調用pathList的dexElements方法把補丁包patch.dex轉化爲Element[]。
7. 兩個Element[]進行合併,把patch.dex放到最前面去。
8. 加載Element[],達到修復目的。
該方案之因此選擇構造函數進行插入代碼,是由於它不增長方法數,一個類即便沒有顯式的構造函數,也會有一個隱式的默認構造函數。
細節:Qzone使用的是在字節碼插入代碼,而不是源代碼插入,使用的是java assist庫來進行字節碼插入的。
互動問題:思考一下,除了經過防止補丁類被打上CLASS_ISPREVERIFIED標誌,咱們還能夠想到有哪些方式來解決Dalvik下的 「unexpected DEX」 異常問題?
優點:
1.沒有合成整包(和微信Tinker比起來),輸出產物比較小,比較靈活。
2.能夠實現類替換,兼容性較高。(某些三星手機不起做用)
不足:
1.不支持即時生效,必須經過重啓才能生效。
2.爲了實現修復這個過程,必須在應用中加入兩個dex, dalvikhack.dex中只有一個類,對性能影響不大,可是對於patch.dex來講,修復的類到了必定數量,就須要花很多的時間加載。對大型應用來講,啓動耗時增長2s以上是很難接受的事。
3.在ART模式下,若是類修改告終構,就會出現內存錯亂的問題。爲了解決這個問題,就必須把全部相關的調用類、父類子類等等所有加載到patch.dex中,這會致使ART下的補丁包異常的大,進一步增長應用啓動加載的時候,耗時更加嚴重。
其中第二點不足,即性能沒法提高的緣由:
插樁的解決方案會影響到運行時性能的緣由在於:app 內的全部補丁類都預埋引用一個獨立 dex 的空類,致使安裝 dexopt 階段的 preverify 失敗,運行時將再次 verify+optimize。
另外即便後期發佈版本實際上無需發佈補丁,咱們也須要預埋插樁的邏輯,這自己也是不合理的一點,因此確實有必要去探索新的方向,既保留補丁的能力,同時去掉插樁帶來的負面影響。
第三點不足,即ART模式下補丁包異常大的緣由:
ART(Android Runtime)是Android在4.4版本中引入的新虛擬機環境,在5.0版本正式取代了Dalvik VM。ART環境下,App安裝時其包含的Dex文件將被dex2oat預編譯成目標平臺的機器碼,從而提升了App的運行效率。在這個預編譯過程當中,dex2oat對目標代碼的優化過程與Dalvik VM下的dexopt有較大區別,尤爲是在5.0版本之後ART環境下新增的方法內聯優化,因爲方法內聯改變了本來的方法分佈和調用流程。
方法內聯之因此會致使優先加載補丁Dex的方案出現問題,本質上是由於補丁Dex只覆蓋了舊Dex裏的一部分類,一旦被覆蓋的類的方法被內聯到了調用者裏,則加載類的過程仍是正常的,即從補丁Dex里加載了新版本的類。但因爲內聯,執行流程並未跳轉到新的方法裏,因而全部關於新版本的類的方法、成員、字符串的查找用的就都是舊方法裏的索引了。所以,在ART模式下,若是類修改告終構,就會出現內存錯亂的問題。爲了解決這個問題,就必須把全部相關的調用類、父類子類等等所有加載到patch.dex中,這會致使ART下的補丁包異常的大,進一步增長應用啓動加載的時候,耗時更加嚴重。
參考文章:
《安卓App熱補丁動態修復技術介紹》 - QQ空間開發團隊
《QFix探索之路》 - 手Q熱補丁輕量級方案