×

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

96
weex暴走青年
2017.01.19 17:24* 字数 982

首先!我们抛开网上的热修复框架不谈,我们来通过原理手动实现一个热修复工具,在撸码之前我们先通过一张图来了解热修复的流程.
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;
        }
    }
}

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

日记本
Web note ad 1