熱更新 補丁包方式(通過學習Qzone的熱修復框架總結)

目錄

實際操作

1、修改有BUG的代碼

2、修改好代碼之後,把這個java文件編譯成.class文件

1)、可以使用編譯工具

2)、通過命令行工具,執行java命令進行編譯

3、打包,把修改好的.class文件使用dx.bat工具打包成

 1)打包jar

2)jar轉換成dex

4、加載dex包到用戶端(通過網絡去自己的服務器下載,測試的時候我們直接放入到手機裏面,通過程序去讀取)

5、(程序讀取)把dex包插入到dexPathList集合中,注意要插入到前面,一般插入到角標0位置

 1)形參patch是補丁的路徑,先去獲取ClassLoader。

2)獲取到DexPathList

3)通過同樣的方法獲取到DexPathList中的集合dexElements,這是dex真正的藏身之地

4)操作dexElements,添加補丁包

6、bug修復

 


 

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 

 

圖片。00001

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中調用,前提是修復包你已經準備並放入了內存中。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章