Android 熱修復其實很簡單

yangxi_001發表於2017-02-08

一、什麼是熱修復

熱修復說白了就是”打補丁”,比如你們公司上線一個app,使用者反應有重大bug,需要緊急修復。如果按照通 
常做法,那就是程式猿加班搞定bug,然後測試,重新打包併發布。這樣帶來的問題就是成本高,效率低。於是,熱 
修復就應運而生.一般通過事先設定的介面從網上下載無Bug的程式碼來替換有Bug的程式碼。這樣就省事多了,用 
戶體驗也好。

二、熱修復的原理

1.Android的類載入機制

Android的類載入器分為兩種,PathClassLoader和DexClassLoader,兩者都繼承自BaseDexClassLoader

PathClassLoader程式碼位於libcore\dalvik\src\main\java\dalvik\system\PathClassLoader.java 
DexClassLoader程式碼位於libcore\dalvik\src\main\java\dalvik\system\DexClassLoader.java 
BaseDexClassLoader程式碼位於libcore\dalvik\src\main\java\dalvik\system\BaseDexClassLoader.java

  • PathClassLoader
  • 用來載入系統類和應用類

  • DexClassLoader

    用來載入jar、apk、dex檔案.載入jar、apk也是最終抽取裡面的Dex檔案進行載入.

    這裡寫圖片描述

2.熱修復機制

看下PathClassLoader程式碼

public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }

    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

DexClassLoader程式碼

public class DexClassLoader extends BaseDexClassLoader {

    public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

兩個ClassLoader就兩三行程式碼,只是呼叫了父類的建構函式.

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在BaseDexClassLoader 建構函式中建立一個DexPathList類的例項,這個DexPathList的建構函式會建立一個dexElements 陣列

public DexPathList(ClassLoader definingContext, String dexPath, String libraryPath, File optimizedDirectory) {
        ... 
        this.definingContext = definingContext;
        ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
        //建立一個陣列
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
        ... 
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

然後BaseDexClassLoader 重寫了findClass方法,呼叫了pathList.findClass,跳到DexPathList類中.

/* package */final class DexPathList {
    ...
    public Class findClass(String name, List<Throwable> suppressed) {
            //遍歷該陣列
        for (Element element : dexElements) {
            //初始化DexFile
            DexFile dex = element.dexFile;

            if (dex != null) {
                //呼叫DexFile類的loadClassBinaryName方法返回Class例項
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }       
        return null;
    }
    ...
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

會遍歷這個陣列,然後初始化DexFile,如果DexFile不為空那麼呼叫DexFile類的loadClassBinaryName方法返回Class例項. 
歸納上面的話就是:ClassLoader會遍歷這個陣列,然後載入這個陣列中的dex檔案. 
而ClassLoader在載入到正確的類之後,就不會再去載入有Bug的那個類了,我們把這個正確的類放在Dex檔案中,讓這個Dex檔案排在dexElements陣列前面即可.

這裡有個問題,可參考QQ空間團隊的 安卓App熱補丁動態修復技術介紹 
概括來講:如果引用者和被引用者的類(直接引用關係)在同一個Dex時,那麼在虛擬機器啟動時,被引用類就會被打上CLASS_ISPREVERIFIED標誌,這樣被引用的類就不能進行熱修復操作了. 
那麼我們就要阻止被引用類打上CLASS_ISPREVERIFIED標誌.QQ空間的方法是在所有引用到該類的建構函式中插入一段程式碼,程式碼引用到別的類.

三、熱修復的例子

我用的是阿里開源的熱修復框架AndFix熱修復框架地址

其實它的原理也是動態載入class檔案,然後呼叫反射完成修復.可參考我上一篇寫的 
Java的ClassLoader載入機制

AndFix是 “Android Hot-Fix”的縮寫。它支援Android 2.3到6.0版本,並且支援arm與X86系統架構的裝置。完美支援Dalvik與ART的Runtime。AndFix 的補丁檔案是以 .apatch 結尾的檔案。

我這是用eclipse寫的Demo.

1.把AndFix抽取成library依賴的形式

這裡寫圖片描述

2.新建一個AndFixDemo專案,依賴AndFix這個library

2.1

新建一個MyApplication繼承Application

public class MyApplication extends Application {

    private static final String TAG = "MyApplication";

    /**
     * apatch檔案
     */
    private static final String APATCH_PATH = "/Dennis.apatch";

    private PatchManager mPatchManager;

    @Override
    public void onCreate() {
        super.onCreate();
        // 初始化
        mPatchManager = new PatchManager(this);
        mPatchManager.init("1.0"); // 版本號

        // 載入 apatch
        mPatchManager.loadPatch();

        //apatch檔案的目錄
        String patchFileString = Environment.getExternalStorageDirectory().getAbsolutePath() + APATCH_PATH;
        File apatchPath = new File(patchFileString);

        if (apatchPath.exists()) {
            Log.i(TAG, "補丁檔案存在");
            try {
                //新增apatch檔案
                mPatchManager.addPatch(patchFileString);
            } catch (IOException e) {
                Log.i(TAG, "打補丁出錯了");
                e.printStackTrace();
            }
        } else {
            Log.i(TAG, "補丁檔案不存在");
        }

    }
}
  • 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
  • 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

實際當中肯定是通過網路介面下載apatch檔案,我這裡為了方便演示就放在了SD卡根目錄

2.2

在MainActivity用一個按鈕彈出吐司,上面是有Bug的程式碼,下面是修正後的程式碼

這裡寫圖片描述

這裡寫圖片描述

分別打包成Bug.apk和NoBug.apk

這裡寫圖片描述

2.3

然後要用到一個生成補丁的工具apkpatch

解壓

這裡寫圖片描述

_MACOSX是給OSX系統用的 
.bat是給window系統用的

我用得是.bat

把之前生成的Bug.apkNoBug.apk,還有打包所使用的keystore檔案放到apkpatch-1.0.3目錄下 
開啟cmd,進入到apkpatch-1.0.3目錄下,輸入如下指令

apkpatch.bat -f NoBug.apk -t Bug.apk -o Dennis -k keystore -p 111111 -a 111111 -e 111111

每個引數含義如下

-f 新版本的apk 
-t 舊版本的apk 
-o 輸出apatch檔案的資料夾,可以隨意命名 
-k 打包的keystore檔名 
-p keystore的密碼 
-a keystore 使用者別名 
-e keystore 使用者別名的密碼

這裡寫圖片描述

如果出現add modified …….就表示成功了,去apkpatch-1.0.3目錄看下,新增了Dennis目錄

這裡寫圖片描述

這裡寫圖片描述

我把這個檔案改為Dennis.apatch

2.4

手機裝上Bug.apk執行起來

這裡寫圖片描述

然後把Dennis.apatch 放到SD卡根目錄,退出app,再進入,按下按鈕

這裡寫圖片描述

最後附上Demo還有apk和apatch 檔案 開啟連結

轉自:http://blog.csdn.net/qq_31530015/article/details/51785228?locationNum=11

相關文章