Android 热修复(全网最简单的热修复讲解)

首先!我们抛开网上的热修复框架不谈,我们来通过原理手动实现一个热修复工具,在撸码之前我们先通过一张图来了解热修复的流程.
Android热修复

ACCCB328-AF5C-4BD9-AD08-6F7D971BD74C.png

聪明的和不聪明的都已经看出来,Android 在加载dex的时候会遍历一个Element集合,找到class,加载成二进制!(别拍砖!)
如果我们想要实现我们的热修复机制,我们只需要把我们的dex补丁插入到集合的最前面(或者插入到bug class 的前面,这里我就偷个懒嘛,反正老天爷是保佑我的嘛!),当遍历开始找到class的时候就直接return了啊,如果集合后面还有bug的dex或者class都不会被加载了啊!看到这里你是不是明白了!热修复就是这么简单!

首先我们需要以下几个队友的配合!

1.PathClassLoader:这个加载器只能加载已经安装的dex文件
2.DexClassLoader:这个加载器能够加载未安装的dex,但是这个dex文件一定要在使用者的App目录中.(原因自己想!)
3.反射工具Filed
4.Android build-tools工具dx(打包dex用的啊)
5.dalvik.system.BaseDexClassLoader 我是一个字符串,对,就是一个字符串.因为我们要反射这个类里面的信息.

我们先看一下BaseDexClassLoader里面的代码,不用担心就看一个方法

1AC4BF7E-5491-4B5E-930E-2B191E9600F6.png

在上一张findClass方法的图

9EBB7355-872B-4352-A089-4FC4C0F95DA2.png

通过看源码你就知道,我上面所说的不是我自己吹牛逼的,也不是忽悠你的!!!

详细的流程:
1.通过PathClassLoader 来加载我们自身App的dex,因为我们要修改自己的bug,而不是隔壁老王的.
2.通过DexClassLoader来加载我们的补丁dex文件,这里面就是没有bug的dex.
3.来!我们先来反射两个classLoader的<DexPathList pathList;>,我们的目的就是拿到这个值.
4.接着我们来继续反射两个classloader中的pathList(注意:是两个!一个是我们自己应用的,另一个是我们补丁的,PathClassLoader和DexClassLoader都继承BaseDexClassLoader),DexPathList里面的<Element[] dexElements;>,没错还是拿到这个数组的值
5.合并两个反射到的Element 数组!这里是重中之重.我们需要把我们的补丁dex放在数组的最前面!
6.将合并的新的数组,通过Field重新设置到我们自身App的DexPathList->dexElements.没错!就是合并之后覆盖有bug那个loader的Element 数组!!
7.通过Android build-tools 中的dx命令打包一个没有bug的dex
注:假设你的App中有一个class A 出bug了,那么你就可以通过dx命令打包一个只有class A的dex文件.

有人说!楼主SB,8步还说全网最简单?呵呵呵呵呵!我只是把代码流程说的详细点而已!不服上代码!只有撸码才是真理!

/**
 * Created by 暴走青年 on 2017/1/19.
 */
public class HotFixEngine {

    public static final String DEX_OPT_DIR = "optimize_dex";//dex的优化路径
    public static final String DEX_BASECLASSLOADER_CLASS_NAME = "dalvik.system.BaseDexClassLoader";
    public static final String DEX_FILE_E = "dex";//扩展名
    public static final String DEX_ELEMENTS_FIELD = "dexElements";//pathList中的dexElements字段
    public static final String DEX_PATHLIST_FIELD = "pathList";//BaseClassLoader中的pathList字段
    public static final String FIX_DEX_PATH = "fix_dex";//fixDex存储的路径


    /**
     * 获得pathList中的dexElements
     *
     * @param obj
     * @return
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    public Object getDexElements(Object obj) throws NoSuchFieldException, IllegalAccessException {
        return getField(obj, obj.getClass(), DEX_ELEMENTS_FIELD);
    }

    public interface LoadDexFileInterruptCallback {
        boolean loadDexFile(File file);
    }
    /**
     * fix
     *
     * @param context
     */
    public void loadDex(Context context, File dexFile) {
        if (context == null) {
            return;
        }
        File fixDir = context.getDir(FIX_DEX_PATH, Context.MODE_PRIVATE);
        //mrege and fix
        mergeDex(context, fixDir,dexFile);
    }

    /**
     * 获取指定classloader 中的pathList字段的值(DexPathList)
     *
     * @param classLoader
     * @return
     */
    public Object getDexPathListField(Object classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        return getField(classLoader, Class.forName(DEX_BASECLASSLOADER_CLASS_NAME), DEX_PATHLIST_FIELD);
    }

    /**
     * 获取一个字段的值
     *
     * @return
     */
    public Object getField(Object obj, Class<?> clz, String fieldName) throws NoSuchFieldException, IllegalAccessException {

        Field field = clz.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(obj);

    }

    /**
     * 为指定对象中的字段重新赋值
     *
     * @param obj
     * @param claz
     * @param filed
     * @param value
     */
    public void setFiledValue(Object obj, Class<?> claz, String filed, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field field = claz.getDeclaredField(filed);
        field.setAccessible(true);
        field.set(obj, value);
//        field.setAccessible(false);
    }

    /**
     * 合并dex
     *
     * @param context
     * @param fixDexPath
     */
    public void mergeDex(Context context, File fixDexPath, File dexFile) {
        try {
            //创建dex的optimize路径
            File optimizeDir = new File(fixDexPath.getAbsolutePath(), DEX_OPT_DIR);
            if (!optimizeDir.exists()) {
                optimizeDir.mkdir();
            }
            //加载自身Apk的dex,通过PathClassLoader
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            //找到dex并通过DexClassLoader去加载
            //dex文件路径,优化输出路径,null,父加载器
            DexClassLoader dexClassLoader = new DexClassLoader(dexFile.getAbsolutePath(), optimizeDir.getAbsolutePath(), null, pathClassLoader);
            //获取app自身的BaseDexClassLoader中的pathList字段
            Object appDexPathList = getDexPathListField(pathClassLoader);
            //获取补丁的BaseDexClassLoader中的pathList字段
            Object fixDexPathList = getDexPathListField(dexClassLoader);

            Object appDexElements = getDexElements(appDexPathList);
            Object fixDexElements = getDexElements(fixDexPathList);
            //合并两个elements的数据,将修复的dex插入到数组最前面
            Object finalElements = combineArray(fixDexElements, appDexElements);
            //给app 中的dex pathList 中的dexElements 重新赋值
            setFiledValue(appDexPathList, appDexPathList.getClass(), DEX_ELEMENTS_FIELD, finalElements);
            Toast.makeText(context, "修复成功!", Toast.LENGTH_SHORT).show();

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

    /**
     * 两个数组合并
     *
     * @param arrayLhs
     * @param arrayRhs
     * @return
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }

    /**
     * 复制SD卡中的补丁文件到dex目录
     */
    public static void copyDexFileToAppAndFix(Context context, String dexFileName, boolean copyAndFix) {
        File path = new File(Environment.getExternalStorageDirectory(), dexFileName);
        if (!path.exists()) {
            Toast.makeText(context, "没有找到补丁文件", Toast.LENGTH_SHORT).show();
            return;
        }
        if (!path.getAbsolutePath().endsWith(DEX_FILE_E)){
            Toast.makeText(context, "补丁文件格式不正确", Toast.LENGTH_SHORT).show();
            return;
        }
        File dexFilePath = context.getDir(FIX_DEX_PATH, Context.MODE_PRIVATE);
        File dexFile = new File(dexFilePath, dexFileName);
        if (dexFile.exists()) {
            dexFile.delete();
        }
        //copy
        InputStream is = null;
        FileOutputStream os = null;
        try {
            is = new FileInputStream(path);
            os = new FileOutputStream(dexFile);
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
            if (dexFile.exists() && copyAndFix) {
                //复制成功,进行修复
                new HotFixEngine().loadDex(context, dexFile);
            }
            path.delete();//删除sdcard中的补丁文件,或者你可以直接下载到app的路径中
            is.close();
            os.close();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (os != null) {
                try {
                    os.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}


如果你能看到这里!那么我告诉你!这里才是最重要的!!
代码已经撸完,并且你的App已经上线,那么我在告诉你怎么打包一个dex
dx --dex --output=在这里指定一个dex的输出路径 在这里指定一个class文件的完整路径,从报名开始的完整路径.(懵逼了吗?),你连dex文件打包命令都不会?你是一个假的Android程序员!不!你是一个假的程序员!
例子:
dx.bat --dex --output=D:\AndroidFix\app\src\main\java D:\AndroidFix\app\src\main\java
如果你爆了一个找不到命令的错误怎么办呢?那么请自行解决!!
打包完了!我们来测试一下!写一个带Bug的类!

public class TestClass {
    public void showToast(String str,Application context){
        Toast.makeText(context,"i am bug!"+1/0,Toast.LENGTH_SHORT).show();

    }

}
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

    }

    /**
     * 关于dex文件被恶意加载和替换的解决方案
     * 1.可通过在服务器生成一个dex文件的MD5列表,在修复之前客户端
     * 向服务发送验证请求,验证通过即可修复。
     * 2.将dex文件打包为rar并且设置密码,在客户端通过ndk进行验证解密
     * @param view
     */
    public void onClick(View view){
        switch (view.getId()){
            case R.id.fix:
                HotFixEngine.copyDexFileToAppAndFix(this,"classes_fix.dex",true);
                break;
            case R.id.bug:
                new TestClass().showToast(null,getApplication());
                break;
        }
    }
}

啥?你还不明白!??请在看一遍!

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

推荐阅读更多精彩内容