Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android熱補丁動態修復實踐

Android熱補丁動態修復實踐

編輯:關於Android編程

前言

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

項目結構

項目結構vc/J7sjrtcTWqsq20OjSqsTjyKXA7b3ioaM8L3A+DQo8aDEgaWQ9"先說原理">先說原理

關於實現原理,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

如果一個類被打上了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類

看圖就好了:

hackdex模塊

將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下
dx工具

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

hack_dex.jar

使用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工具轉化,然後路徑一定要對

patch_dex.jar就是我們的補丁包,這裡我們為了演示把它放到項目的assets目錄下:

補丁包

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

打補丁過程

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

總結

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

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved