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或者不足,可以即时告知我,我会即时修改。

猜你喜欢

转载自blog.csdn.net/qxs965266509/article/details/50390325