Android熱補丁動態修複實踐

來源:互聯網
上載者:User

Android熱補丁動態修複實踐
前言

好幾個月之前關於Android App熱補丁修複火了一把,源於QQ空間團隊的一篇文章安卓App熱補丁動態修複技術介紹,然後各大廠的開源項目都出來了,本文的實踐基於HotFix,也就是QQ空間技術團隊那篇文章所應用的技術,筆者會把整個過程的細節和思路在文章中詳說,研究這個的出發點也是為了能緊急修複app的bug,而不需要重複發包,不需要使用者重新下載app就能把問題解決,個人覺得這個還是蠻有價值的,雖然老闆不知道….。

項目結構

這裡筆者建立一個新的項目”HotFixDemo”,帶大家一步一步來完成Hotfix這個架構實現,這個項目包含以下module:
- app :我們的Android應用程式Module。
- buildsrc :使用Groovy實現的項目,提供了一個類,用來實現修改class檔案的操作。
- hackdex :提供了一個類,後面會用來打包成hack.dex,也是buildsrc裡面實現在所有類的建構函式插入的一段代碼所引用到的類。
- hotfixlib :這個module最終會被app關聯,裡面提供實現熱補丁的核心方法

這個Demo裡面的代碼跟HotFix架構基本無異,主要是告訴大家它實現的過程,如果光看代碼,不實踐是無法把它應用到你自己的app上去的,因為有很多比較深入的知識需要你去理解。

先說原理

關於實現原理,QQ空間那篇文章已經說過了,這裡我再重新闡述一遍:
- Android使用的是PathClassLoader作為其類的載入器
- 一個ClassLoader可以包含多個dex檔案,每個dex檔案是一個Element,多個dex排列成一個有序的dexElements數組
- 當找類的時候會遍曆dexElements數組,從dex檔案中找類,找到則返回,否則繼續下一個dex檔案尋找
- 熱補丁的方案,其實就是將有問題的類單獨打包成一個dex檔案(如:patch.dex),然後將這個dex插入到dexElements數組的最前面去。

ok,這個就是HotFix對app進行熱補丁的原理,其實就是用ClassLoader載入機制,覆蓋掉有問題的方法,然後我們所謂的補丁就是將有問題的類打成一個包

再說問題

當然要實現熱補丁動態修複不會很容易,我們首要解決的一個問題是:

當虛擬機器啟動時,當verify選項被開啟時,如果static方法、private方法、建構函式等,其中的直接引用(第一層關係)到的類都在同一個dex檔案中,那麼該類會被打上CLASS_ISPREERIFIED標記

如所示:

如果一個類被打上了CLASS_ISPREERIFIED這個標誌,如果該類引用的另外一個類在另一個dex檔案,就會報錯。簡單來說,就是你在打補丁之前,你所修複的類已經被打上標記,你通過補丁去修複bug的時候這個時候你就不能完成校正,就會報錯。

解決問題

要解決上一節所提到的問題就要在apk打包之前就阻止相關類打上CLASS_ISPREERIFIED標誌,解決方案如下:
在所有類的建構函式插入一段代碼,如:

public class BugClass {    public BugClass() {        System.out.println(AntilazyLoad.class);    }    public String bug() {        return "bug class";    }}

其中引用到的AntilazyLoad這個類會單獨打包成hack.dex,這樣當安裝apk的時候,classes.dex內的類都會引用一個不相同的dex中的AntilazyLoad類,這樣就解決CLASS_ISPREERIFIED標記問題了。

實現細節

上面幾節講完原理、之後拋出了問題,再提出解決方案,相信大家對整個熱補丁修複架構有了一定的認識,至少我們知道它到底是怎麼一回事。下面來講實現細節:

建立兩個類
package com.devilwwj.hotfixdemo;/** * com.devilwwj.hotfixdemo * Created by devilwwj on 16/3/8. */public class BugClass {    public String bug() {        return "bug class";    }}
package com.devilwwj.hotfixdemo;/** * com.devilwwj.hotfixdemo * Created by devilwwj on 16/3/8. */public class LoadBugClass {    public String getBugString() {        BugClass bugClass = new BugClass();        return bugClass.bug();    }}

我們需要做的是在這兩個類的class檔案的構造方法中插入一段代碼:

System.out.println(AntilazyLoad.class);
建立hackdex模組並建立AntilazyLoad類

看圖就好了:

將AntilazyLoad單獨打成hack_dex.jar包

通過以下命令來實現:

jar cvf hack.jar com.devilwwj.hackdex/*

這個命令會將AntilazyLoad類打包成hack.jar檔案

dx --dex --output hack_dex.jar hack.jar

這個命令使用dx工具對hack.jar進行轉化,產生hack_dex.jar檔案

dx工具在我們的sdk/build-tools下

最終我們把hack_dex.jar檔案放到項目的assets目錄下:

使用javassist實現動態代碼注入

建立buildSrc模組,這個項目是使用Groovy開發的,需要配置Groovy SDK才可以編譯成功。
在這裡下載Groovy SDK:http://groovy-lang.org/download.html,下載之後,設定項目user Library即可。

它裡面提供了一個方法,用來向指定類的建構函式注入代碼:

 /**     * 植入代碼     * @param buildDir 是項目的build class目錄,就是我們需要注入的class所在地     * @param lib 這個是hackdex的目錄,就是AntilazyLoad類的class檔案所在地     */    public static void process(String buildDir, String lib) {        println(lib);        ClassPool classes = ClassPool.getDefault()        classes.appendClassPath(buildDir)        classes.appendClassPath(lib)        // 將需要關聯的類的構造方法中插入引用代碼        CtClass c = classes.getCtClass("com.devilwwj.hotfixdemo.BugClass")        if (c.isFrozen()) {            c.defrost()        }        println("====添加構造方法====")        def constructor = c.getConstructors()[0];        constructor.insertBefore("System.out.println(com.devilwwj.hackdex.AntilazyLoad.class);")        c.writeFile(buildDir)        CtClass c1 = classes.getCtClass("com.devilwwj.hotfixdemo.LoadBugClass")        if (c1.isFrozen()) {            c1.defrost()        }        println("====添加構造方法====")        def constructor1 = c1.getConstructors()[0];        constructor1.insertBefore("System.out.println(com.devilwwj.hackdex.AntilazyLoad.class);")        c1.writeFile(buildDir)    }
配置app項目的build.gradle

上一小節建立的module提供相應的方法來讓我們對項目的類進行代碼注入,我們需要在build.gradle來配置讓它自動來做這件事:

apply plugin: 'com.android.application'task('processWithJavassist') << {    String classPath = file('build/intermediates/classes/debug')// 項目編譯class所在目錄    com.devilwwj.patch.PatchClass.process(classPath, project(':hackdex').buildDir.absolutePath + "/intermediates/classes/debug") // 第二個參數是hackdex的class所在目錄}android {    compileSdkVersion 23    buildToolsVersion "23.0.1"    defaultConfig {        applicationId "com.devilwwj.hotfixdemo"        minSdkVersion 14        targetSdkVersion 23        versionCode 1        versionName "1.0"    }    buildTypes {        debug {            minifyEnabled false            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }        release {            minifyEnabled false            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'        }    }    applicationVariants.all { variant ->        variant.dex.dependsOn << processWithJavassist // 在執行dx命令之前將代碼打入到class中    }}dependencies {    compile fileTree(dir: 'libs', include: ['*.jar'])    testCompile 'junit:junit:4.12'    compile 'com.android.support:appcompat-v7:23.1.1'    compile 'com.android.support:design:23.1.1'    compile project(':hotfixlib')}

這時候我們run項目,反編譯build/output/apk下的app-debug.apk檔案,你就可以看到代碼已經成功植入了。

mac下的反編譯工具:
https://sourceforge.net/projects/jadx/?source=typ_redirect

反編譯的結果如:

其實你也可以直接在項目中看:

建立hotfixlib模組,並關聯到項目中

這差不多是最後一步了,也是最核心的一步,提供將heck_dex.jar動態插入到dexElements的方法。

核心代碼:

package com.devilwwj.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;/** * com.devilwwj.hotfixlib * Created by devilwwj on 16/3/9. */public final class HotFix {    public static void patch(Context context, String patchDexFile, String patchClassName) {        if (patchDexFile != null && new File(patchDexFile).exists()) {            try {                if (hasLexClassLoader()) {                    injectAliyunOs(context, patchDexFile, patchClassName);                } else if (hasDexClassLoader()) {                    injectAboveEqualApiLevel14(context, patchDexFile, patchClassName);                } else {                    injectBelowApiLevel14(context, patchDexFile, patchClassName);                }            } catch (Throwable th) {            }        }    }    private static boolean hasLexClassLoader() {        try {            Class.forName("dalvik.system.LexClassLoader");            return true;        } catch (ClassNotFoundException e) {            e.printStackTrace();            return false;        }    }    private static boolean hasDexClassLoader() {        try {            Class.forName("dalvik.system.BaseDexClassLoader");            return true;        } catch (ClassNotFoundException e) {            e.printStackTrace();            return false;        }    }    private static void injectAliyunOs(Context context, String patchDexFile, String patchClassName) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {        PathClassLoader obj = (PathClassLoader) context.getClassLoader();        String replaceAll = new File(patchDexFile).getName().replaceAll("\\.[a-zA-Z0-9]+", ".lex");        Class cls = Class.forName("dalvik.system.LexClassLoader");        Object newInstance = cls.getConstructor(new Class[]{String.class, String.class, String.class, ClassLoader.class}).newInstance(                new Object[]{context.getDir("dex", 0).getAbsolutePath()                        + File.separator + replaceAll, context.getDir("dex", 0).getAbsolutePath(), patchDexFile, obj});        cls.getMethod("loadClass", new Class[]{String.class}).invoke(newInstance, new Object[]{patchClassName});        setField(obj, PathClassLoader.class, "mPaths", appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(newInstance, cls, "mRawDexPath")));        setField(obj, PathClassLoader.class, "mFiles", combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(newInstance, cls, "mFiles")));        setField(obj, PathClassLoader.class, "mZips", combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(newInstance, cls, "mZips")));        setField(obj, PathClassLoader.class, "mLexs", combineArray(getField(obj, PathClassLoader.class, "mLexs"), getField(newInstance, cls, "mDexs")));    }    @TargetApi(14)    private static void injectBelowApiLevel14(Context context, String str, String str2) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {        PathClassLoader obj = (PathClassLoader) context.getClassLoader();        DexClassLoader dexClassLoader = new DexClassLoader(str, context.getDir("dex", 0).getAbsolutePath(), str, context.getClassLoader());        dexClassLoader.loadClass(str2);        setField(obj, PathClassLoader.class, "mPaths",                appendArray(getField(obj, PathClassLoader.class, "mPaths"), getField(dexClassLoader, DexClassLoader.class, "mRawDexPath")));        setField(obj, PathClassLoader.class, "mFiles",                combineArray(getField(obj, PathClassLoader.class, "mFiles"), getField(dexClassLoader, DexClassLoader.class, "mFiles")));        setField(obj, PathClassLoader.class, "mZips",                combineArray(getField(obj, PathClassLoader.class, "mZips"), getField(dexClassLoader, DexClassLoader.class, "mZips")));        setField(obj, PathClassLoader.class, "mDexs",                combineArray(getField(obj, PathClassLoader.class, "mDexs"), getField(dexClassLoader, DexClassLoader.class, "mDexs")));        obj.loadClass(str2);    }    /**     * 將dex注入dexElements數組中     * @param context     * @param str     * @param str2     * @throws ClassNotFoundException     * @throws NoSuchFieldException     * @throws IllegalAccessException     */    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);    }    /**     * 通過PathClassLoader拿到pathList     * @param obj     * @return     * @throws ClassNotFoundException     * @throws NoSuchFieldException     * @throws IllegalAccessException     */    private static Object getPathList(Object obj) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {        return getField(obj, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");    }    /**     * 通過pathList取得dexElements對象     * @param obj     * @return     * @throws NoSuchFieldException     * @throws IllegalAccessException     */    private static Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {        return getField(obj, obj.getClass(), "dexElements");    }    /**     * 通過反射拿到指定對象     * @param obj     * @param cls     * @param str     * @return     * @throws NoSuchFieldException     * @throws IllegalAccessException     */    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);    }    /**     * 通過反射設定屬性     * @param obj     * @param cls     * @param str     * @param obj2     * @throws NoSuchFieldException     * @throws IllegalAccessException     */    private static void setField(Object obj, Class cls, String str, Object obj2) throws NoSuchFieldException, IllegalAccessException {        Field declaredField = cls.getDeclaredField(str);        declaredField.setAccessible(true);        declaredField.set(obj, obj2);    }    /**     * 合并數組     * @param obj     * @param obj2     * @return     */    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;    }    /**     * 添加到數組     * @param obj     * @param obj2     * @return     */    private static Object appendArray(Object obj, Object obj2) {        Class componentType = obj.getClass().getComponentType();        int length = Array.getLength(obj);        Object newInstance = Array.newInstance(componentType, length + 1);        Array.set(newInstance, 0, obj2);        for (int i = 0; i < length + 1; i++) {            Array.set(newInstance, i, Array.get(obj, i - 1));        }        return newInstance;    }}
準備補丁,最後測試結果

補丁是我們程式修複bug的包,如果我們已經上線的包出現了bug,你需要緊急修複,那你就找到有bug的那個類,將它修複,然後將這個修複的class檔案打包成jar包,讓服務端將這個補丁包放到指定位置,你的就程式就可以將這補丁包下載到sdcard,之後就是程式自動幫你打補丁把問題修複。

比如我們上面提到的BugClass:
未修複之前:

public class BugClass {    public String bug() {        return "bug class";    }}

修複之後:

public class BugClass {    public String bug() {        return "小巫將bug修複啦!!!";    }}

你要做的就是替換這個類,怎麼做?

先打包:

記住:一定要經過dx工具轉化,然後路徑一定要對<喎?http://www.bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPnBhdGNoX2RleC5qYXK+zcrHztLDx7XEsrm2obD8o6zV4sDvztLDx86qwcvR3cq+sNHL/LfFtb3P7sS/tcRhc3NldHPEv8K8z8KjujwvcD4NCjxwPjxpbWcgYWx0PQ=="補丁包" src="http://www.bkjia.com/uploads/allimg/160316/042002D31-8.png" title="\" />

之後,就是測試效果了,看動態圖:

好,到這裡就大公告成了,我們的bug被修複了啦。

總結

本次實踐過程是基於HotFix架構,在這裡感謝開源的作者,因為不滿足於拿作者的東西直接用,然後不知道為什麼,所以筆者把整個過程都跑了一遍,正所謂實踐出真知,原本以為很難的東西通過反覆實踐也會變得不那麼難,期間實踐自然不會那麼順利,筆者就遇到一個坑,比如Groovy編譯,hack_dex包中的類找不到等等,但最後都一一解決了,研究完這個熱更新架構,再去研究其他熱更新架構也是一樣的,因為原理都一樣,所以就不糾結研究哪個了,之後筆者也會把這個技術用到項目中去,不用每次發包也是心情愉悅的,最後感謝各位看官耐心看,有啥問題就直接留言吧。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.