天天看點

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檔案,如下:

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

紅色框标注的部分就是MainActivity build完成後生成class檔案,如果你想問我怎麼是三個class檔案,原因是:onCreate中有2個注冊的點選時間監聽器,每個監聽器都生成了一個新的class檔案。

把相關修複bug的類的class檔案複制到一個檔案下(fixbugdex),目前我也儲存了class所在的包。如下:

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

以下,拷貝方式不可取:

如果在工具中看class檔案,效果如下:

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

這裡看到的僅有一個MainActivity.class,切記,這是工具為了友善你檢視class檔案,顯示上做了處理。不能從此位置單獨一個個class檔案拷貝,例如從此位置單獨拷貝出MainActivity.class,這個class就不是完成的類了,檔案内容如下:

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

是以此方式不可取,當然可以直接拷貝整個包。

然後cmd到剛才的fixbugdex檔案

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

然後執行指令 jar cvf fixbug.jar cn/*

這條指令就是把cn下的所有檔案打包到fixbug.jar檔案中

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

執行完成後:

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

檢視fixbug.jar内容:

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

接下,需要把jar檔案轉換成dex檔案:

工具:dx

下載下傳工具并解壓到AndroidSdk–>platform-tools目錄

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

同時,也可以把fixbug.jar檔案拷貝到AndroidSdk–>platform-tools目錄,然後你也可以使用絕對路徑.

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

注:–output 後面可以接絕對路徑。

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

執行完成後的結果:

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

檢視一下patch.jar檔案的内容:

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

打開應用執行Toast按鈕:

我為了測試友善,把patch.jar檔案放到了sdcard根目錄中,當然也可以選擇網上下載下傳的方式,其實都是一樣的。

啟動後效果如下:

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

然後,點選加載更新檔檔案,并推出應用重新進入,并點選Toast按鈕:

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

到此,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());
           

清除所有更新檔檔案的接口:

到此接口已介紹完。

中途遇到的坑:

執行

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 = ;

    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 = ; i < patchFiles.length; i++) {
                if (patchFiles[i].getName().lastIndexOf(".jar") == patchFiles[i]
                        .getName().length() - ) {// 僅處理.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 = ; i < patchFiles.length; i++) {
                if (patchFiles[i].getName().lastIndexOf(".jar") == patchFiles[i]
                        .getName().length() - ) {
                    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, , BUF_SIZE)) > ) {
                digests.update(buf, , len);
                dexWriter.write(buf, , len);
            }
            dexWriter.close();
            bis.close();
            BigInteger bi = new BigInteger(, digests.digest());
            String result = bi.toString();

            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 = ; 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或者不足,可以即時告知我,我會即時修改。

繼續閱讀