美文网首页androidAndroid热修复技术android开发技巧
Android 热修复(全网最简单的热修复讲解)

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

作者: weex暴走青年 | 来源:发表于2017-01-19 17:24 被阅读10649次

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

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

相关文章

网友评论

  • jionkang:如果类包含了资源id,每次打包的资源id是变的,这种的类比如一个activity就不行了吧
  • c50f81762420:多谢作者总结分享
  • c08d9829e293:华为6.0 vivo 7.0 无效
  • Candy有雪吃:特意注册了简书来评论的,文章能看懂,前面看了鸿洋大神的热修复文章,看的云里雾里的(实在对不起自己这份工作),看评论有人说没成功,具体我也没试,晚上回去试试。
  • JasonBourneLee:哇哇哇,成功了哇,好开心,谢谢楼主:smile:
  • 沉默寡言若有所思:然后文中有一处错误,应是
    获取app自身的PathClassLoader中的pathList字段
  • 沉默寡言若有所思:其实这篇文章写得还不错,就是再平和一些不要这么暴走就好了。。不过也许这就是你的风格吧,无所谓了:) 评论里有些人的问题看看能不能帮他们解决一下 output is input
  • 夏大王2019:你好 有测试demo吗,我在android 7.0 和android 4.4 上测试失败, 提示修复成功了但是,执行还是执行的老代码。 谢谢~~
    书中自有黄:红米 1s android 4.4.2也失败了 提示修复成功了但是,执行还是执行的老代码
    c63af5959b6b:我也是执行老代码,机器:红米Note4,系统:Android6.0
  • d76b5f6a4c5c:试了,有引用重复这个问题:
    java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
    at com.viviant.hotfixdemo.MainActivity.onClick(MainActivity.java:33)
  • jjzxcjj123:讲的不错,但是运行不成功。亲测,5.1.1系统和5.0.1系统均不支持。
    weex暴走青年:@cmm451739644 如果增加新的资源或者别的东西,看我的增量更新那篇。动态增加资源最好使用虚拟dom的开发框架。
    1446be8a39a0:@weex暴走青年 运行了一下,可以使用。现在只是支持逻辑错误,热修复,如果是新增功能,增加了图片资源和布局文件什么的,这个还是不能用。我的理解对吗?请问下
    weex暴走青年:@jjzxcjj123 没问题啊.我的测试机就是OPPO 的5.1 我测试了修复和增量都没问题.
  • Ansel_Young:热修复技术,支持
  • 1446be8a39a0:不是需要给类打一个标记,我记得
    weex暴走青年:代码调整了一下,去掉了for循环遍历本地文件.和for循环合并数组.
    1446be8a39a0: @weex暴走青年 好的,谢谢,这个不错,一定试试,谢谢。
    weex暴走青年:@cmm451739644 你可以试试代码。

本文标题:Android 热修复(全网最简单的热修复讲解)

本文链接:https://www.haomeiwen.com/subject/tjmibttx.html