Android-FixBug熱修復框架的使用及源碼分析(不發版修復bug)

前面幾篇博文已經介紹了2種熱修復框架的使用及源碼分析,AndFix兼容性比較好,而Dexposed Art處於Beta版。

AndFix和Dexposed都是阿里的開源項目。
Alibaba-AndFix Bug熱修復框架的使用
Alibaba-AndFix Bug熱修復框架原理及源碼解析
Alibaba-Dexposed框架在線熱補丁修復的使用
Alibaba-Dexposed Bug框架原理及源碼解析

今天主要介紹的框架是根據騰訊的博客使用ClassLoader寫的熱修復框架。
騰訊博客:【新技能get】讓App像Web一樣發佈新版本

首先,看需要修復的類部分:

package cn.coolspan.open.fixbug;

import java.io.File;

import android.app.Activity;
import android.os.Bundle;
import android.os.Environment;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Toast;

/**
 * MainActivity 2015-12-22 下午10:30:57
 *
 * @author 喬曉松 [email protected]
 */
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.button1).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View view) {
                MyApplication myApplication = (MyApplication) getApplication();
                File patch = new File(
                        Environment.getExternalStorageDirectory(), "patch.jar");
                Log.e("file:", "" + patch.exists());
                myApplication.fixBugManage.addPatch(patch.getAbsolutePath());
            }
        });
        findViewById(R.id.button2).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View view) {
            //修復此位置的bug
                Toast.makeText(MainActivity.this, "...bug...",
                        Toast.LENGTH_SHORT).show();
            }
        });
    }
}

以上是手機上安裝的apk存在bug的位置。

接下,是修復完成Bug後的類:

......
......
findViewById(R.id.button2).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View view) {
                Toast.makeText(MainActivity.this, "fix...bug...",
                        Toast.LENGTH_SHORT).show();
            }
        });
......
......

修復好了,build工程(工具也會自動build),把java編譯成class文件。

由於我使用debug模式調試安裝的apk,所以我build完成後找的也是debug下的class文件,如下:
這裏寫圖片描述
紅色框標註的部分就是MainActivity build完成後生成class文件,如果你想問我怎麼是三個class文件,原因是:onCreate中有2個註冊的點擊時間監聽器,每個監聽器都生成了一個新的class文件。

把相關修復bug的類的class文件複製到一個文件下(fixbugdex),當前我也保存了class所在的包。如下:
這裏寫圖片描述

以下,拷貝方式不可取:
如果在工具中看class文件,效果如下:

這裏寫圖片描述

這裏看到的僅有一個MainActivity.class,切記,這是工具爲了方便你查看class文件,顯示上做了處理。不能從此位置單獨一個個class文件拷貝,例如從此位置單獨拷貝出MainActivity.class,這個class就不是完成的類了,文件內容如下:
這裏寫圖片描述

所以此方式不可取,當然可以直接拷貝整個包。
然後cmd到剛纔的fixbugdex文件
這裏寫圖片描述
然後執行命令 jar cvf fixbug.jar cn/*
這條命令就是把cn下的所有文件打包到fixbug.jar文件中
這裏寫圖片描述
執行完成後:
這裏寫圖片描述
查看fixbug.jar內容:
這裏寫圖片描述

接下,需要把jar文件轉換成dex文件:
工具:dx

下載工具並解壓到AndroidSdk–>platform-tools目錄
這裏寫圖片描述
同時,也可以把fixbug.jar文件拷貝到AndroidSdk–>platform-tools目錄,然後你也可以使用絕對路徑.

cmd到AndroidSdk–>platform-tools目錄下執行命令:

dx --dex --output patch.jar fixbug.jar

注:–output 後面可以接絕對路徑。
這裏寫圖片描述

執行完成後的結果:
這裏寫圖片描述

查看一下patch.jar文件的內容:
這裏寫圖片描述

打開應用執行Toast按鈕:
我爲了測試方便,把patch.jar文件放到了sdcard根目錄中,當然也可以選擇網上下載的方式,其實都是一樣的。

啓動後效果如下:
這裏寫圖片描述

然後,點擊加載補丁文件,並推出應用重新進入,並點擊Toast按鈕:
這裏寫圖片描述

到此,bug已經被修復完成。

Api接口介紹:

首先,把FixBugManage.java文件引入到你的項目中

首先,在自定義Application中初始化:
init(versionCode);
當versionCode與之前的versionCode不同,會自動清除掉之前addPatch所有的補丁文件
當versionCode與之前的versionCode相同,會自動加載之前addPatch所有的補丁文件

package cn.coolspan.open.fixbug;

import android.app.Application;

public class MyApplication extends Application {

    public FixBugManage fixBugManage;

    @Override
    public void onCreate() {
        super.onCreate();
        this.fixBugManage = new FixBugManage(this);
        this.fixBugManage.init("1.0");
    }
}

添加新補丁文件接口:
addPatch(patchPath);

 MyApplication myApplication = (MyApplication) getApplication();
                File patch = new File(
                        Environment.getExternalStorageDirectory(), "patch.jar");
                myApplication.fixBugManage.addPatch(patch.getAbsolutePath());

清除所有補丁文件的接口:

removeAllPatch();

到此接口已介紹完。

中途遇到的坑:

執行jar cvf fixbug.jar cn/*
異常:bad class file magic (cafebabe) or version (0033.0000)
解決方法:在build.gradle中jdk的版本修改爲1.6

 compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_6
        targetCompatibility JavaVersion.VERSION_1_6
    }

FixBugManage源碼分析:

package cn.coolspan.open.fixbug;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.math.BigInteger;
import java.security.MessageDigest;

import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

import android.content.Context;
import android.content.SharedPreferences;

/**
 * FixBugManage 2015-12-22 下午9:59:28
 *
 * @author 喬曉松 [email protected]
 */
public class FixBugManage {

    private Context context;

    private static final int BUF_SIZE = 2048;

    private File patchs;
    private File patchsOptFile;

    public FixBugManage(Context context) {
        this.context = context;
        this.patchs = new File(this.context.getFilesDir(), "patchs");// 存放補丁文件
        this.patchsOptFile = new File(this.context.getFilesDir(), "patchsopt");// 存放預處理補丁文件壓縮處理後的dex文件
    }

    /**
     * 初始化版本號
     *
     * @param versionCode
     */
    public void init(String versionCode) {
        SharedPreferences sharedPreferences = this.context
                .getSharedPreferences("fixbug", Context.MODE_PRIVATE);
        String oldVersionCode = sharedPreferences
                .getString("versionCode", null);
        if (oldVersionCode == null
                || !oldVersionCode.equalsIgnoreCase(versionCode)) {
            this.initPatchsDir();// 初始化補丁文件目錄
            this.clearPaths();// 清楚所有的補丁文件
            sharedPreferences.edit().clear().putString("versionCode", versionCode)
                    .commit();// 存儲版本號
        } else {
            this.loadPatchs();// 加載已經添加的補丁文件(.jar)
        }
    }

    /**
     * 讀取補丁文件夾並加載
     */
    private void loadPatchs() {
        if (patchs.exists() && patchs.isDirectory()) {// 判斷文件是否存在並判斷是否是文件夾
            File patchFiles[] = patchs.listFiles();// 獲取文件夾下的所有的文件
            for (int i = 0; i < patchFiles.length; i++) {
                if (patchFiles[i].getName().lastIndexOf(".jar") == patchFiles[i]
                        .getName().length() - 4) {// 僅處理.jar文件
                    this.loadPatch(patchFiles[i].getAbsolutePath());// 加載jar文件
                }
            }
        } else {
            this.initPatchsDir();
        }
    }

    /**
     * 加載單個補丁文件
     *
     * @param patchPath
     */
    private void loadPatch(String patchPath) {
        try {
            injectDexAtFirst(patchPath, patchsOptFile.getAbsolutePath());// 讀取jar文件中dex內容
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * patch所在文件目錄
     *
     * @param patchPath
     */
    public void addPatch(String patchPath) {
        File inFile = new File(patchPath);
        File outFile = new File(patchs, inFile.getName());
        this.copyFile(outFile, inFile);
        this.loadPatch(patchPath);
    }

    /**
     * 移除所有的patch文件
     */
    public void removeAllPatch() {
        this.clearPaths();
    }

    /**
     * 清除所有的補丁文件
     */
    private void clearPaths() {
        if (patchs.exists() && patchs.isDirectory()) {
            File patchFiles[] = patchs.listFiles();
            for (int i = 0; i < patchFiles.length; i++) {
                if (patchFiles[i].getName().lastIndexOf(".jar") == patchFiles[i]
                        .getName().length() - 4) {
                    patchFiles[i].delete();
                }
            }
        }
    }

    /**
     * 初始化存放補丁的文件目錄
     */
    private void initPatchsDir() {
        if (!this.patchs.exists()) {
            this.patchs.mkdirs();
        }
        if (!this.patchsOptFile.exists()) {
            this.patchsOptFile.mkdirs();
        }
    }

    /**
     * 複製文件從inFile到outFile
     *
     * @param outFile
     * @param inFile
     * @return
     */
    private boolean copyFile(File outFile, File inFile) {
        BufferedInputStream bis = null;
        OutputStream dexWriter = null;
        try {
            MessageDigest digests = MessageDigest.getInstance("MD5");

            bis = new BufferedInputStream(new FileInputStream(inFile));
            dexWriter = new BufferedOutputStream(new FileOutputStream(outFile));
            byte[] buf = new byte[BUF_SIZE];
            int len;
            while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
                digests.update(buf, 0, len);
                dexWriter.write(buf, 0, len);
            }
            dexWriter.close();
            bis.close();
            BigInteger bi = new BigInteger(1, digests.digest());
            String result = bi.toString(16);

            File toFile = new File(outFile.getParentFile(), result + ".jar");
            outFile.renameTo(toFile);
            return true;
        } catch (Exception e) {
            if (dexWriter != null) {
                try {
                    dexWriter.close();
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                }
            }
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException ioe) {
                    ioe.printStackTrace();
                }
            }
            return false;
        }
    }

    public static void injectDexAtFirst(String dexPath, String defaultDexOptPath)
            throws NoSuchFieldException, IllegalAccessException,
            ClassNotFoundException {
        DexClassLoader dexClassLoader = new DexClassLoader(dexPath,
                defaultDexOptPath, dexPath, getPathClassLoader());// 把dexPath文件補丁處理後放入到defaultDexOptPath目錄中
        Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));// 獲取當面應用Dex的內容
        Object newDexElements = getDexElements(getPathList(dexClassLoader));// 獲取補丁文件Dex的內容
        Object allDexElements = combineArray(newDexElements, baseDexElements);// 把當前apk的dex和補丁文件的dex進行合併
        Object pathList = getPathList(getPathClassLoader());// 獲取當前的patchList對象
        setField(pathList, pathList.getClass(), "dexElements", allDexElements);// 利用反射設置對象的值
    }

    private static PathClassLoader getPathClassLoader() {
        PathClassLoader pathClassLoader = (PathClassLoader) FixBugManage.class
                .getClassLoader();// 獲取類加載器
        return pathClassLoader;
    }

    private static Object getDexElements(Object paramObject)
            throws IllegalArgumentException, NoSuchFieldException,
            IllegalAccessException {
        return getField(paramObject, paramObject.getClass(), "dexElements");// 利用反射獲取到dexElements屬性
    }

    private static Object getPathList(Object baseDexClassLoader)
            throws IllegalArgumentException, NoSuchFieldException,
            IllegalAccessException, ClassNotFoundException {
        return getField(baseDexClassLoader,
                Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");// 利用反射獲取到pathList屬性
    }

    /**
     * 此方法是合併2個數組,把補丁dex中的內容放到數組最前,達到修復bug的目的
     *
     * @param firstArray
     * @param secondArray
     * @return
     */
    private static Object combineArray(Object firstArray, Object secondArray) {
        Class<?> localClass = firstArray.getClass().getComponentType();
        int firstArrayLength = Array.getLength(firstArray);
        int allLength = firstArrayLength + Array.getLength(secondArray);
        Object result = Array.newInstance(localClass, allLength);
        for (int k = 0; k < allLength; ++k) {
            if (k < firstArrayLength) {
                Array.set(result, k, Array.get(firstArray, k));
            } else {
                Array.set(result, k,
                        Array.get(secondArray, k - firstArrayLength));
            }
        }
        return result;
    }

    public static Object getField(Object obj, Class<?> cl, String field)
            throws NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);// 強制反射
        return localField.get(obj);// 獲取值
    }

    public static void setField(Object obj, Class<?> cl, String field,
                                Object value) throws NoSuchFieldException,
            IllegalArgumentException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);// 強制反射
        localField.set(obj, value);// 設置值
    }
}

如有bug或者不足,可以即時告知我,我會即時修改。

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