1、概述 最新github上開源了不少熱補丁動態修復框架,大體有: https://github.com/dodola/HotFix https://github.com/jasonross/Nuwa https://github.com/bunnyblue/DroidFix 上述三個框架呢,根據其描述,原理都來自:安卓App熱補丁動態修復技術介紹,以及Android dex分包方案,因此這倆篇務必要看。這裏就不對三個框架作過多對比了,由於原理都一致,實現的代碼可能差別並非特別大。 有興趣的直接看這篇原理文章,加上上面框架的源碼基本就能夠看懂了。固然了,本篇博文也會作個上述框架源碼的解析,以及在整個實現過程當中用到的技術的解析。 2、熱修復原理 對於熱修復的原理,若是你看了上面的兩篇文章,相信你已經大概明白了。重點須要知道的就是,Android的ClassLoader體系,android中加載類通常使用的是PathClassLoader和DexClassLoader,首先看下這兩個類的區別: 對於PathClassLoader,從文檔上的註釋來看: Provides a simple {
@link ClassLoader} implementation that operates on a list of files and directories in the local file system, but does not attempt to load classes from the network. Android uses this class for its system class loader and for its application class loader(s). 能夠看出,Android是使用這個類做爲其系統類和應用類的加載器。而且對於這個類呢,只能去加載已經安裝到Android系統中的apk文件。 對於DexClassLoader,依然看下注釋: A class loader that loads classes from {
@code .jar} and {
@code .apk} files containing a {
@code classes.dex} entry. This can be used to execute code not installed as part of an application. 能夠看出,該類呢,能夠用來從.jar和.apk類型的文件內部加載classes.dex文件。能夠用來執行非安裝的程序代碼。 ok,若是你們對於插件化有所瞭解,確定對這個類不陌生,插件化通常就是提供一個apk(插件)文件,而後在程序中load該apk,那麼如何加載apk中的類呢?其實就是經過這個DexClassLoader,具體的代碼咱們後面有描述。 ok,到這裏,你們只須要明白,Android使用PathClassLoader做爲其類加載器,DexClassLoader能夠從.jar和.apk類型的文件內部加載classes.dex文件就行了。 上面咱們已經說了,Android使用PathClassLoader做爲其類加載器,那麼熱修復的原理具體是? ok,對於加載類,無非是給個classname,而後去findClass,咱們看下源碼就明白了。 PathClassLoader和DexClassLoader都繼承自BaseDexClassLoader。在BaseDexClassLoader中有以下源碼: #BaseDexClassLoader @Override protected Class findClass(String name) throws ClassNotFoundException { Class clazz = pathList.findClass(name); if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; } #DexPathList 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; } #DexFile public Class loadClassBinaryName(String name, ClassLoader loader) { return defineClass(name, loader, mCookie); } private native static Class defineClass(String name, ClassLoader loader, int cookie); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 能夠看出呢,BaseDexClassLoader中有個pathList對象,pathList中包含一個DexFile的集合dexElements,而對於類加載呢,就是遍歷這個集合,經過DexFile去尋找。 ok,通俗點說: 一個ClassLoader能夠包含多個dex文件,每一個dex文件是一個Element,多個dex文件排列成一個有序的數組dexElements,當找類的時候,會按順序遍歷dex文件,而後從當前遍歷的dex文件中找類,若是找類則返回,若是找不到從下一個dex文件繼續查找。(來自:安卓App熱補丁動態修復技術介紹) 那麼這樣的話,咱們能夠在這個dexElements中去作一些事情,好比,在這個數組的第一個元素放置咱們的patch.jar,裏面包含修復過的類,這樣的話,當遍歷findClass的時候,咱們修復的類就會被查找到,從而替代有bug的類。 說到這,你可能已經露出笑容了,原來熱修復原理這麼簡單。不過,還存在一個CLASS_ISPREVERIFIED的問題,對於這個問題呢,詳見:安卓App熱補丁動態修復技術介紹該文有圖文詳解。 ok,對於CLASS_ISPREVERIFIED,仍是帶你們理一下: 根據上面的文章,在虛擬機啓動的時候,當verify選項被打開的時候,若是static方法、private方法、構造函數等,其中的直接引用(第一層關係)到的類都在同一個dex文件中,那麼該類就會被打上CLASS_ISPREVERIFIED標誌。 那麼,咱們要作的就是,阻止該類打上CLASS_ISPREVERIFIED的標誌。 注意下,是阻止引用者的類,也就是說,假設你的app裏面有個類叫作LoadBugClass,再其內部引用了BugClass。發佈過程當中發現BugClass有編寫錯誤,那麼想要發佈一個新的BugClass類,那麼你就要阻止LoadBugClass這個類打上CLASS_ISPREVERIFIED的標誌。 也就是說,你在生成apk以前,就須要阻止相關類打上CLASS_ISPREVERIFIED的標誌了。對於如何阻止,上面的文章說的很清楚,讓LoadBugClass在構造方法中,去引用別的dex文件,好比:hack.dex中的某個類便可。 ok,總結下: 其實就是兩件事:一、動態改變BaseDexClassLoader對象間接引用的dexElements;二、在app打包的時候,阻止相關類去打上CLASS_ISPREVERIFIED標誌。 若是你沒有看明白,沒事,多看幾遍,下面也會經過代碼來講明。 3、阻止相關類打上CLASS_ISPREVERIFIED標誌 ok,接下來的代碼基本上會經過https://github.com/dodola/HotFix所提供的代碼來說解。 那麼,這裏拿具體的類來講: 大體的流程是:在dx工具執行以前,將LoadBugClass.class文件呢,進行修改,再其構造中添加System.out.println(dodola.hackdex.AntilazyLoad.class),而後繼續打包的流程。注意:AntilazyLoad.class這個類是獨立在hack.dex中。 ok,這裏你們可能會有2個疑問: 如何去修改一個類的class文件 如何在dx以前去進行疑問1的操做 (1)如何去修改一個類的class文件 這裏咱們使用javassist來操做,很簡單: ok,首先咱們新建幾個類: package dodola.hackdex; public class AntilazyLoad { } package dodola.hotfix; public class BugClass { public String bug() { return "bug class"; } } package dodola.hotfix; public class LoadBugClass { public String getBugString() { BugClass bugClass = new BugClass(); return bugClass.bug(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 注意下,這裏的package,咱們要作的是,上述類正常編譯之後產生class文件。好比:LoadBugClass.class,咱們在LoadBugClass.class的構造中去添加一行: System.out.println(dodola.hackdex.AntilazyLoad.class) 1 1 下面看下操做類: package test; import javassist.ClassPool; import javassist.CtClass; import javassist.CtConstructor; public class InjectHack { public static void main(String[] args) { try { String path = "/Users/zhy/develop_work/eclipse_android/imooc/JavassistTest/"; ClassPool classes = ClassPool.getDefault(); classes.appendClassPath(path + "bin");//項目的bin目錄便可 CtClass c = classes.get("dodola.hotfix.LoadBugClass"); CtConstructor ctConstructor = c.getConstructors()[0]; ctConstructor .insertAfter("System.out.println(dodola.hackdex.AntilazyLoad.class);"); c.writeFile(path + "/output"); } catch (Exception e) { e.printStackTrace(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ok,點擊run便可了,注意項目中導入javassist-*.jar的包。 首先拿到ClassPool對象,而後添加classpath,若是你有多個classpath能夠屢次調用。而後從classpath中找到LoadBugClass,拿到其構造方法,在其最後插入一行代碼。ok,代碼很好懂。 ok,咱們反編譯看下咱們生成的class文件: ok,關於javassist,若是有興趣的話,你們能夠參考幾篇文章學習下: http://www.ibm.com/developerworks/cn/java/j-dyn0916/ http://zhxing.iteye.com/blog/1703305 (2)如何在dx以前去進行(1)的操做 ok,這個就結合https://github.com/dodola/HotFix的源碼來講了。 將其源碼導入以後,打開app/build.gradle apply plugin: 'com.android.application' task('processWithJavassist') << { String classPath = file('build/intermediates/classes/debug')//項目編譯class所在目錄 dodola.patch.PatchClass.process(classPath, project(':hackdex').buildDir .absolutePath + '/intermediates/classes/debug')//第二個參數是hackdex的class所在目錄 } android { applicationVariants.all { variant -> variant.dex.dependsOn << processWithJavassist //在執行dx命令以前將代碼打入到class中 } } 1 2 3 4 5 6 7 8 9 10 11 12 13 1 2 3 4 5 6 7 8 9 10 11 12 13 你會發現,在執行dx以前,會先執行processWithJavassist這個任務。這個任務的做用呢,就和咱們上面的代碼一致了。並且源碼也給出了,你們本身看下。 ok,到這呢,你就能夠點擊run了。ok,有興趣的話,你能夠反編譯去看看dodola.hotfix.LoadBugClass這個類的構造方法中是否已經添加了改行代碼。 關於反編譯的用法,工具等,參考:http://blog.csdn.net/lmj623565791/article/details/23564065 ok,到此咱們已經可以正常的安裝apk而且運行了。可是目前還未涉及到打補丁的相關代碼。 4、動態改變BaseDexClassLoader對象間接引用的dexElements ok,這裏就比較簡單了,動態改變一個對象的某個引用咱們反射就能夠完成了。 不過這裏須要注意的是,還記得咱們以前說的,尋找class是遍歷dexElements;而後咱們的AntilazyLoad.class實際上並不包含在apk的classes.dex中,而且根據上面描述的須要,咱們須要將AntilazyLoad.class這個類打成獨立的hack_dex.jar,注意不是普通的jar,必須通過dx工具進行轉化。 具體作法: jar cvf hack.jar dodola/hackdex/* dx --dex --output hack_dex.jar hack.jar 1 2 1 2 若是,你沒有辦法把那一個class文件搞成jar,去百度一下… ok,如今有了hack_dex.jar,這個是幹嗎的呢? 應該還記得,咱們的app中部門類引用了AntilazyLoad.class,那麼咱們必須在應用啓動的時候,降這個hack_dex.jar插入到dexElements,不然確定會出事故的。 那麼,Application的onCreate方法裏面就很適合作這件事情,咱們把hack_dex.jar放到assets目錄。 下面看hotfix的源碼: /* * Copyright (C) 2015 Baidu, Inc. All Rights Reserved. */ package dodola.hotfix; import android.app.Application; import android.content.Context; import java.io.File; import dodola.hotfixlib.HotFix; /** * Created by sunpengfei on 15/11/4. */ public class HotfixApplication extends Application { @Override public void onCreate() { super.onCreate(); File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar"); Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar"); HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad"); try { this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 ok,在app的私有目錄建立一個文件,而後調用Utils.prepareDex將assets中的hackdex_dex.jar寫入該文件。 接下來HotFix.patch就是去反射去修改dexElements了。咱們深刻看下源碼: /* * Copyright (C) 2015 Baidu, Inc. All Rights Reserved. */ package dodola.hotfix; /** * Created by sunpengfei on 15/11/4. */ public class Utils { private static final int BUF_SIZE = 2048; public static boolean prepareDex(Context context, File dexInternalStoragePath, String dex_file) { BufferedInputStream bis = null; OutputStream dexWriter = null; bis = new BufferedInputStream(context.getAssets().open(dex_file)); dexWriter = new BufferedOutputStream(new FileOutputStream(dexInternalStoragePath)); byte[] buf = new byte[BUF_SIZE]; int len; while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) { dexWriter.write(buf, 0, len); } dexWriter.close(); bis.close(); return true; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ok,其實就是文件的一個讀寫,將assets目錄的文件,寫到app的私有目錄中的文件。 下面主要看patch方法 /* * Copyright (C) 2015 Baidu, Inc. All Rights Reserved. */ package dodola.hotfixlib; import android.annotation.TargetApi; import android.content.Context; import java.io.File; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import dalvik.system.DexClassLoader; import dalvik.system.PathClassLoader; /* compiled from: ProGuard */ public final class HotFix { public static void patch(Context context, String patchDexFile, String patchClassName) { if (patchDexFile != null && new File(patchDexFile).exists()) { try { if (hasLexClassLoader()) { injectInAliyunOs(context, patchDexFile, patchClassName); } else if (hasDexClassLoader()) { injectAboveEqualApiLevel14(context, patchDexFile, patchClassName); } else { injectBelowApiLevel14(context, patchDexFile, patchClassName); } } catch (Throwable th) { } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 這裏很據系統中ClassLoader的類型作了下判斷,原理都是反射,咱們看其中一個分支hasDexClassLoader(); private static boolean hasDexClassLoader() { try { Class.forName("dalvik.system.BaseDexClassLoader"); return true; } catch (ClassNotFoundException e) { return false; } } private static void injectAboveEqualApiLevel14(Context context, String str, String str2) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader(); Object a = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList( new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader())))); Object a2 = getPathList(pathClassLoader); setField(a2, a2.getClass(), "dexElements", a); pathClassLoader.loadClass(str2); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 首先查找類dalvik.system.BaseDexClassLoader,若是找到則進入if體。 在injectAboveEqualApiLevel14中,根據context拿到PathClassLoader,而後經過getPathList(pathClassLoader),拿到PathClassLoader中的pathList對象,在調用getDexElements經過pathList取到dexElements對象。 ok,那麼咱們的hack_dex.jar如何轉化爲dexElements對象呢? 經過源碼能夠看出,首先初始化了一個DexClassLoader對象,前面咱們說過DexClassLoader的父類也是BaseDexClassLoader,那麼咱們能夠經過和PathClassLoader一樣的方式取得dexElements。 ok,到這裏,咱們取得了,系統中PathClassLoader對象的間接引用dexElements,以及咱們的hack_dex.jar中的dexElements,接下來就是合併這兩個數組了。 能夠看到上面的代碼使用的是combineArray方法。 合併完成後,將新的數組經過反射的方式設置給pathList. 接下來看一下反射的細節: private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList"); } private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException { return getField(obj, obj.getClass(), "dexElements"); } private static Object getField(Object obj, Class cls, String str) throws NoSuchFieldException, IllegalAccessException { Field declaredField = cls.getDeclaredField(str); declaredField.setAccessible(true); return declaredField.get(obj); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 其實都是取成員變量的過程,應該很容易懂~~ private static Object combineArray(Object obj, Object obj2) { Class componentType = obj2.getClass().getComponentType(); int length = Array.getLength(obj2); int length2 = Array.getLength(obj) + length; Object newInstance = Array.newInstance(componentType, length2); for (int i = 0; i < length2; i++) { if (i < length) { Array.set(newInstance, i, Array.get(obj2, i)); } else { Array.set(newInstance, i, Array.get(obj, i - length)); } } return newInstance; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ok,這裏的兩個數組合並,只須要注意一件事,將hack_dex.jar裏面的dexElements放到新數組前面便可。 到此,咱們就完成了在應用啓動的時候,動態的將hack_dex.jar中包含的DexFile注入到ClassLoader的dexElements中。這樣就不會查找不到AntilazyLoad這個類了。 ok,那麼到此呢,仍是沒有看到咱們如何打補丁,哈,其實呢,已經說過了,打補丁的過程和咱們注入hack_dex.jar是一致的。 你如今運行HotFix的app項目,點擊menu裏面的測試: 會彈出:調用測試方法:bug class 接下來就看如何完成熱修復。 5、完成熱修復 ok,那麼咱們假設BugClass這個類有錯誤,須要修復: package dodola.hotfix; public class BugClass { public String bug() { return "fixed class"; } } 1 2 3 4 5 6 7 8 9 1 2 3 4 5 6 7 8 9 能夠看到字符串變化了:bug class -> fixed class . 而後,編譯,將這個類的class->jar->dex。步驟和上面是一致的。 jar cvf path.jar dodola/hotfix/BugClass.class dx --dex --output path_dex.jar path.jar 1 2 1 2 拿到path_dex.jar文件。 正常狀況下,這個玩意應該是下載獲得的,固然咱們介紹原理,你能夠直接將其放置到sdcard上。 而後在Application的onCreate中進行讀取,咱們這裏爲了方便也放置到assets目錄,而後在Application的onCreate中添加代碼: public class HotfixApplication extends Application { @Override public void onCreate() { super.onCreate(); File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar"); Utils.prepareDex(this.getApplicationContext(), dexPath, "hack_dex.jar"); HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad"); try { this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad"); } catch (ClassNotFoundException e) { e.printStackTrace(); } dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "path_dex.jar"); Utils.prepareDex(this.getApplicationContext(), dexPath, "path_dex.jar"); HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hotfix.BugClass"); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 其實就是添加了後面的3行,這裏須要說明一下,第一行依舊是複製到私有目錄,若是你是sdcard上,那麼操做基本是一致的,這裏就別問:若是在sdcard或者網絡上怎麼處理~ ok,那麼再次運行咱們的app。 ok,最後說一下,說項目中有一個打補丁的按鈕,在menu下,那麼你也能夠不在Application裏面添加咱們最後的3行。 你運行app後,先點擊打補丁,而後點擊測試也能夠發現成功修復了。 若是先點擊測試,再點擊打補丁,再測試是不會變化的,由於類一旦加載之後,不會從新再去從新加載了。 ok,到此,咱們的熱修復的原理,已經解決方案,我相信已經很詳細的介紹完成了,若是你有足夠的耐心必定能夠實現。中間製做補丁等操做,咱們的操做比較麻煩,自動化的話,能夠參考https://github.com/jasonross/Nuwa。