Android热更新十:自己写一个Android热修复

很早之前就想深入的研究和学习一下热修复,由于时间的原因一直拖着,现在才执笔弄起来。


Android而更新系列:
Android热更新一:JAVA的类加载机制
Android热更新二:理解Java反射
Android热更新三:Android类加载机制
Android热更新四:热修复机制
Android热更新五:四大热修复方案分析
Android热更新六:Qzone热更新原理
Android热更新七:Tinker热更新原理
Android热更新八:AndFix热更新原理
Android热更新九:Robust热更新原理
Android热更新十:自己写一个Android热修复


经过之前分析了各大热修复的实现原理,参考原理,我们来写一个属于自己的Android热修复吧。

一. 热修复简述。

所谓热修复,就是已经上线APP发现了Bug,不需要花大精力发布新版本,即可通过在线下载补丁并且修复Bug。

热修复的基本原理:

Android框架中存在一个数组,它的作用是维护全部的dex文件(我们写的类的二进制表述方式,用来给安卓虚拟机加载),安卓虚拟机会根据需要从该数组按照自上而下的顺序加载对应的类文件,即使数组中存多个同一个类对应的dex文件,虚拟机一旦找到了对应的dex文件就会停止查找,并加载。根据这个规则,我们只需要把Bug修复涉及到的类文件插入到数组的最前面去,就可以达到修复的目的。

说白了,热修复是利用Android Application的加载dex的规则,从中干预,从而达到修复的目的。

二. 根据原理,我们先来写一个热修复的核心类,

有了上面的原理分析,这个类也肯定不会太复杂,主要用到的是Java的反射以及ClassLoader(DexClassLoader以及PathClassLoader)。

package com.yb.demo.olfix.fixdex;

import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;

import android.content.Context;

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

/**
 * 作者:created by yufenfen on 2019/3/21:12:13
 * 邮箱: ybyj1314@126.com
 */
public final class HotFix {
    /**
     * 修复指定的类
     *
     * @param context        上下文对象
     * @param fixDexFilePath   修复的dex文件路径
     */
    public static void fixDexFile(Context context, String fixDexFilePath) {
        if (fixDexFilePath != null && new File(fixDexFilePath).exists()) {
            try {
                injectDexToClassLoader(context, fixDexFilePath);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * @param context
     * @param fixDexFilePath 修复文件的路径
     * @throws ClassNotFoundException
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    private static void injectDexToClassLoader(Context context, String fixDexFilePath)
            throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        //读取 baseElements
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
        Object basePathList = getPathList(pathClassLoader);
        Object baseElements = getDexElements(basePathList);

        //读取 fixElements
        String baseDexAbsolutePath = context.getDir("dex", 0).getAbsolutePath();
        DexClassLoader fixDexClassLoader = new DexClassLoader(
                fixDexFilePath, baseDexAbsolutePath, fixDexFilePath, context.getClassLoader());
        Object fixPathList = getPathList(fixDexClassLoader);
        Object fixElements = getDexElements(fixPathList);

        //合并两份Elements
        Object newElements = combineArray(baseElements, fixElements);

        //一定要重新获取,不要用basePathList,会报错
        Object basePathList2 = getPathList(pathClassLoader);

        //新的dexElements对象重新设置回去
        setField(basePathList2, basePathList2.getClass(), "dexElements", newElements);
    }

    /**
     * 通过反射先获取到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");
    }

    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);
    }

    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);
    }

    /**
     * 合拼dexElements ,并确保 fixElements 在 baseElements 之前
     *
     * @param baseElements
     * @param fixElements
     * @return
     */
    private static Object combineArray(Object baseElements, Object fixElements) {
        Class componentType = fixElements.getClass().getComponentType();
        int length = Array.getLength(fixElements);
        int length2 = Array.getLength(baseElements) + length;
        Object newInstance = Array.newInstance(componentType, length2);
        for (int i = 0; i < length2; i++) {
            if (i < length) {
                Array.set(newInstance, i, Array.get(fixElements, i));
            } else {
                Array.set(newInstance, i, Array.get(baseElements, i - length));
            }
        }
        return newInstance;
    }
}

三. 写bug修bug

修复主体类写好了,那么,我们来写个baseAPP,,然后在baseAPP里写一个专门带有bug的类,既然要测试热修复,我们肯定要写一个带有bug的类。

package com.yb.demo.olfix;

import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;

import java.net.URL;

/**
 * 作者:created by yufenfen on 2019/3/27:08:26
 * 邮箱: ybyj1314@126.com
 */
public class FixMe {
    private final String TAG = "FixMe";
    private ImageView mBelle;
    private TextView mNotGril;

    private Context mContext;
    private MyGlide myGlide;

    //false: bug, true: fix
    private boolean fix = false;

    public FixMe(Activity context) {
        mContext = context;
        mBelle = (ImageView) context.findViewById(R.id.gril);
        mNotGril = (TextView) context.findViewById(R.id.notgril);
        myGlide = MyGlide.getInstance(mContext);

    }

    public void showWhat() {
        if (fix) {
            fixBug();
            Log.d(TAG, "fix bug!");
        } else {
            mBelle.setVisibility(View.GONE);
            mNotGril.setVisibility(View.VISIBLE);
            Log.d(TAG, "this is a bug!");
        }
    }

    private void fixBug() {
        try {

            mBelle.setVisibility(View.VISIBLE);
            mNotGril.setVisibility(View.GONE);

            URL url = new URL("https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1553252483041&di=3c51ed29d8b2efe3c98dac5168f19e6b&imgtype=0&src=http%3A%2F%2Fpic.feizl.com%2Fupload%2Fallimg%2F171016%2F522zpd0y2srfqa.jpg");
            myGlide.loadImageAndAddToTarget(mBelle, url);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

然后打包安装到我们的设备上。


接下来,我们把bug修正,也就是在出bug的对应类修复bug,其实就是上面的类的变量赋值为true

    //false: bug, true: fix
    private boolean fix = true;

四. 打补丁包打补丁

修复好bug后,先不要着急编译运行,我们要先在AndroidStudio里面关闭掉Instant_Run。
由于Android Studio的instan run的原理也是热修复,所以安装的时候不会安装完整的安装包,只会安装新改变的代码。


Jietu20190321-155822.jpg

重新编译,然后就可以打热修复补丁包了,我们这里了非常原始的打补丁包的方式,步骤如下:

1. 拷贝出新修改的类

点击Build->RebuildProject来重新构建,构建完成之后,可以在app/build/interintermediate/debug/包名/找到你刚刚修改的class文件,将他拷贝出来,要连同包名路径一起拷贝出来。


Jietu20190322-095337.jpg

2. 将class文件打包成dex文件

我们前面知道热修复的原理是Dalvik/ART加载dex文件,所以接下来我们要将class文件打包成dex文件,首先我们找到AndroidSDK的build-tools 目录下,在控制台下进入该目录下的任意一个版本,执行dx命令,关于dx命令的使用帮助可以使用dx -- help,下面们通过 dx --dex [指定输出路径]/classes.dex [刚才拷贝的修复bug的类及包名的目录]这样我们就得到了.dex文件。

dx --dex --output=/Users/yufenfen/Desktop/outputdex/classes2.dex /Users/yufenfen/Desktop/outputdex

3. 将补丁包放到目的地

由于实现在线下载补丁文件(classes2.dex)还是比较麻烦一点,我们这里采取简单粗暴的方式,就是手动的把补丁放到以下目录:

Environment.getExternalStorageDirectory()

四. 调用热更新

放好补丁文件后,就可以在页面点击按钮“修复”后进行检查热修复,方式如下

      private void checkFix(){
        try {
            String dexPath = Environment.getExternalStorageDirectory() + "/classes2.dex";
            HotFix.fixDexFile(this, dexPath);

            Toast.makeText(this, "修复成功", Toast.LENGTH_SHORT).show();
        } catch (Exception e) {
            Toast.makeText(this, "修复失败" + e.getMessage(), Toast.LENGTH_SHORT).show();
            e.printStackTrace();
        }
    }

OK,造补丁包打补丁就是这么简单粗暴的搞定了,接下来就可以验证是否成功了。
先验证APP是在bug状态的,然后重新启动有bug的APP,点击修复,我们就可以看到美女了。


存在的问题,是如果先点击“来个妞”,再点修复,貌似没有效果,一定要先点修复,再点击“来个妞”才好,原因待确定。
初步估计是相关的类已经被加载,因为根据类加载机制,尽管修复bug了,也就是对相关的类进行了提位操作,而该类已经被加载内存,不会再重新加载,故无效。
有时间找找准确的原因,找到后会更新。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270