目錄
2、修改好代碼之後,把這個java文件編譯成.class文件
3、打包,把修改好的.class文件使用dx.bat工具打包成
4、加載dex包到用戶端(通過網絡去自己的服務器下載,測試的時候我們直接放入到手機裏面,通過程序去讀取)
5、(程序讀取)把dex包插入到dexPathList集合中,注意要插入到前面,一般插入到角標0位置
1)形參patch是補丁的路徑,先去獲取ClassLoader。
3)通過同樣的方法獲取到DexPathList中的集合dexElements,這是dex真正的藏身之地
Qzone熱修復技術是一個冷啓動修復方式,爲什麼是冷啓動呢,這需要根據它的實現原理來解析這個問題:
這種修復方式在於我們的代碼編譯成class文件之後,並打包成的dex文件的運行機制。我們都知道我們的虛擬機在執行我們的代碼的時候,是從dex包中獲取class文件進行讀取的,而一個apk不止有一個dex文件,尋找class文件的時候,通過循環遍歷所有dex文件,找到了某個class,無論後面有沒有文件,循環都會中止,通過這個原理 我們可以使用插入補丁包的方式,把補丁dex包插入到dex集合最前面,這樣虛擬機在尋找之前出bug的文件之前,首先會找到這個補丁包下面的 我們已經修改過的class,原先的有bug的dex不會被執行到,問題也就解決了
實際操作
理論是理論,實際的還需要實戰去操作,我們先說一下具體的操作流程:
1、修改有BUG的代碼
這一步,就是更改代碼,把有問題的java類裏面有問題的的代碼更改過來,無論幾個類
2、修改好代碼之後,把這個java文件編譯成.class文件
這一步也很簡單,
1)、可以使用編譯工具
build一下,然後在 項目➡️APP➡️build➡️intermediates➡️classes文件夾debug或者release中找到對應的已經編譯好的.class文件(在編譯工具的工具欄有一個 build 按鈕,點擊彈出列表找到build apk(s),點擊)
2)、通過命令行工具,執行java命令進行編譯
javac classpath/class1.java classpath/class2.java
(文件之間需要空一格,以便區分)
3、打包,把修改好的.class文件使用dx.bat工具打包成
dx.bat工具位於sdk目錄下的build-tools下(Android\sdk\build-tools\27.0.3)
打包命令:
dx --dex [--output=<file>] [<file>.class | <file>.{zip,jar,apk} | <directory>]
例如: --dex --output=/Users/**/Desktop/testDex.dex TextUtil.class TextUtil2.class
第一個參數是打包後的dex放置位置,後面的是需要打包的class文件 多個文件之間用空格隔開
注意 如果提示 dx命令找不到。說明沒配置環境變量 要麼去配置環境變量或者去de .bat的目錄中去運行這個命令
我是先把.class文件打包成jar格式 然後在轉換成dex
提醒大家一句:我的打包jar跟轉換dex,都是在項目的debug或者release文件下操作的,(否則會引起java文件的路徑問題)
1)打包jar
jar cvf patch.jar com/jjf/hotfixdemo2/TextUtil.class com/jjf/hotfixdemo2/TextUtil.class com/jjf/hotfixdemo2/TextUtil.class
2)jar轉換成dex
dx --dex --output=patch.dex patch.jar
因爲所有的操作都在debug或者release下操作的,而我們是直接指定的文件名 並沒有寫路徑,所以產生的文件都在當前目錄下,還有一個原因,每個java類都有一個
package com.**.**;
來說明自己在項目中的路徑,如果不在這裏操作,編譯成.dex文件時會提示java文件路徑錯誤(如果只是測試兩個java類,不涉及Android,跟本文章瘦的熱更新無關,可以刪掉這行去手動編譯成class,並打包如果其中有集成JFrame,並在main方法調用了窗口,則可以直接雙擊這個jar運行,會出現一個你編輯的窗口)
4、加載dex包到用戶端(通過網絡去自己的服務器下載,測試的時候我們直接放入到手機裏面,通過程序去讀取)
因爲是做demo,這一步我直接把打包好的dex文件放入了提前準備好的手機內存中的某一路徑,在代碼中直接去讀取這個路徑,
public class MyApplication extends Application { @SuppressLint("SdCardPath") @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base);//hotfixTextAddress 測試熱修復補丁地址 String address = Environment.getExternalStorageDirectory() + "/測試熱修復補丁地址/patch.jar"; File file = new File(address); Log.i("TextUtil", "" + file.exists()); if (file.exists()) { //把布丁插入到pathList中(pathList進程變量,一個存放dex編譯文件的集合) HiAppFix.installPatch(this, address); } } }
這個加載的步驟,最好放在啓動頁 初始化的時候去完成,在實際項目中的流程:
5、(程序讀取)把dex包插入到dexPathList集合中,注意要插入到前面,一般插入到角標0位置
這一步是核心的代碼。涉及到了代碼具體加載的步驟,直接上代碼,然後再一步一步解讀,大家也可以去看看我畫的熱更新思維導圖,裏面有對源碼的解釋總結
private static final String TAG = "TextUtil"; /** * 執行修復 */ public static void installPatch(Context context, String patch) { //優化目錄必須是私有目錄 File cacheDir = context.getCacheDir(); //PathClassLoader ClassLoader classLoader = context.getClassLoader(); try { //先獲取pathList屬性 Field pathList = SharedReflectUtils.getField(classLoader, "pathList"); //通過屬性反射獲取屬性的對象 DexPathList Object pathListObject = pathList.get(classLoader); //通過 pathListObject 對象獲取 pathList類中的dexElements 屬性 //原本的dex element數組 Field dexElementsField = SharedReflectUtils.getField(pathListObject, "dexElements"); //通過dexElementsField 屬性獲取它存在的對象 Object[] dexElementsObject = (Object[]) dexElementsField.get(pathListObject); List<File> files = new ArrayList<>(); File file = new File(patch);//補丁包 if(file.exists()){ files.add(file); } //插樁所用到的類 // files.add(antiazyFile); Method method = SharedReflectUtils.getMethod(pathListObject, "makeDexElements", List.class, File.class, List.class,ClassLoader.class); final List<IOException> suppressedExceptionList = new ArrayList<IOException>(); //補丁的element數組 Object[] patchElement = (Object[]) method.invoke(null, files, cacheDir, suppressedExceptionList, classLoader); //用於替換系統原本的element數組 Object[] newElement = (Object[]) Array.newInstance(dexElementsObject.getClass().getComponentType(), dexElementsObject.length + patchElement.length); //合併複製element System.arraycopy(patchElement, 0, newElement, 0, patchElement.length); System.arraycopy(dexElementsObject, 0, newElement, patchElement.length, dexElementsObject.length); // 替換 dexElementsField.set(pathListObject,newElement); } catch (NoSuchFieldException | IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); Log.i(TAG,"installPatch="+e.toString()); } }
/** *修復工具類(反射) */ public class SharedReflectUtils { /** * 反射獲取某個屬性 * * @param instance * @param name * @return */ public static Field getField(Object instance, String name) throws NoSuchFieldException { for (Class<?> cls = instance.getClass(); cls != null; cls = cls.getSuperclass()) { try { Field declaredField = cls.getDeclaredField(name); //如果反射獲取的類 方法 屬性不是public 需要設置權限 declaredField.setAccessible(true); return declaredField; } catch (NoSuchFieldException e) { e.printStackTrace(); } } throw new NoSuchFieldException("Field: " + name + " not found in " + instance.getClass()); } /** * 反射獲取某個屬性 * * @param instance * @param name * @return */ public static Method getMethod(Object instance, String name,Class<?>... parameterTypes) throws NoSuchFieldException { for (Class<?> cls = instance.getClass(); cls != null; cls = cls.getSuperclass()) { try { Method methodMethod = cls.getDeclaredMethod(name,parameterTypes); //如果反射獲取的類 方法 屬性不是public 需要設置權限 methodMethod.setAccessible(true); return methodMethod; } catch (NoSuchMethodException e) { e.printStackTrace(); } } throw new NoSuchFieldException("Field: " + name + " not found in " + instance.getClass()); } }
1)形參patch是補丁的路徑,先去獲取ClassLoader。
因爲類加載器是完成熱更新的核心類,這裏我們要強調一下,類加載器ClassLoader 有兩個子類BootClassLoader屬於加載系統源碼,我們不必理會,BaseDexClassLoader纔是我們要關注的,這個類裏面什麼都沒有,我們去使用過的是他的字類 DexClassLoader、PathClassLoader,DexClassLoader在API26之後,代碼已經與PathClassLoader完全相同,但是默認使用PathClassLoader這個加載器去加載類
2)獲取到DexPathList
private final DexPathList pathList; /** * @hide */ public void addDexPath(String dexPath) { pathList.addDexPath(dexPath, null /*optimizedDirectory*/); }
上面是BaseDexClassLoader中關於pathList 的源碼,可以看出它的作用
Field.get(Object obj) 返回指定對象obj上此 Field 表示的字段的值
通過屬性方法Field的get獲取這個屬性在ClassLoader中的值pathListObject,這纔是真正的獲取到了 DexPathList 的對象pathList,然後我們要通過這個對象來操作
3)通過同樣的方法獲取到DexPathList中的集合dexElements,這是dex真正的藏身之地
/** * Constructs an instance. 116 * 117 * @param definingContext the context in which any as-yet unresolved 118 * classes should be defined 119 * @param dexPath list of dex/resource path elements, separated by 120 * {@code File.pathSeparator} 121 * @param librarySearchPath list of native library directory path elements, 122 * separated by {@code File.pathSeparator} 123 * @param optimizedDirectory directory where optimized {@code .dex} files 124 * should be found and written to, or {@code null} to use the default 125 * system directory for same 126 */ 127 public DexPathList(ClassLoader definingContext, String dexPath, 128 String librarySearchPath, File optimizedDirectory) { 129 130 if (definingContext == null) { 131 throw new NullPointerException("definingContext == null"); 132 } 133 134 if (dexPath == null) { 135 throw new NullPointerException("dexPath == null"); 136 } 137 138 if (optimizedDirectory != null) { 139 if (!optimizedDirectory.exists()) { 140 throw new IllegalArgumentException( 141 "optimizedDirectory doesn't exist: " 142 + optimizedDirectory); 143 } 144 145 if (!(optimizedDirectory.canRead() 146 && optimizedDirectory.canWrite())) { 147 throw new IllegalArgumentException( 148 "optimizedDirectory not readable/writable: " 149 + optimizedDirectory); 150 } 151 } 152 153 this.definingContext = definingContext; 154 155 ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); 156 // save dexPath for BaseDexClassLoader 157 this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, 158 suppressedExceptions, definingContext); 159 160 // Native libraries may exist in both the system and 161 // application library paths, and we use this search order: 162 // 163 // 1. This class loader's library path for application libraries (librarySearchPath): 164 // 1.1. Native library directories 165 // 1.2. Path to libraries in apk-files 166 // 2. The VM's library path from the system property for system libraries 167 // also known as java.library.path 168 // 169 // This order was reversed prior to Gingerbread; see http://b/2933456. 170 this.nativeLibraryDirectories = splitPaths(librarySearchPath, false); 171 this.systemNativeLibraryDirectories = 172 splitPaths(System.getProperty("java.library.path"), true); 173 List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories); 174 allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories); 175 176 this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories); 177 178 if (suppressedExceptions.size() > 0) { 179 this.dexElementsSuppressedExceptions = 180 suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]); 181 } else { 182 dexElementsSuppressedExceptions = null; 183 } 184 }
我們要把補丁加載到這個集合當中去,可以借鑑addDexPath方法裏面的流程
從上面的源碼碼可以看出,可以通過makeDexElements得到Element[]數組,那我們就把補丁傳入反射出這個方法,並運行他,得到這個數組
4)操作dexElements,添加補丁包
關鍵代碼在於這幾行
Object[] patchElement = (Object[]) method.invoke(null, files, cacheDir, suppressedExceptionList, classLoader); //用於替換系統原本的element數組 Object[] newElement = (Object[]) Array.newInstance(dexElementsObject.getClass().getComponentType(), dexElementsObject.length + patchElement.length); //合併複製element System.arraycopy(patchElement, 0, newElement, 0, patchElement.length); System.arraycopy(dexElementsObject, 0, newElement, patchElement.length, dexElementsObject.length); // 替換 dexElementsField.set(pathListObject,newElement);
最後的 dexElementsField.set(pathListObject,newElement); 不能忘記 這行代碼的意思是 設置某對象(第一個參數)下的某個屬性(第二個參數)的值,這裏我們相當於直接把原來的數組覆蓋了
6、bug修復
好了到了這裏,我們就可以重啓app
總結:這只是簡單的實現熱修復的其中一個方式,而且和沒有具體涉及到業務部分,所以看起來很簡單,實際上在熱修復中還有一個不可忽視的問題,就是如果想要去適配4.4及之前的版本,還需要使用到插樁技術,這個涉及到了4.4 之前的編譯優化問題,在後面的版本取消掉了,並且插樁技術在美團的熱修復中也使用到了,今天說到的Qzone熱修復屬於冷啓動修復,美團熱修復技術屬於熱啓動修復。下節我們再說有關插樁的部分-------------如果具體修復部分的代碼沒看懂,可以直接複製第五節5、(程序讀取)把dex包插入到dexPathList集合中,注意要插入到前面,一般插入到角標0位置 這個裏面的方法在Application中調用,前提是修復包你已經準備並放入了內存中。