热修复(一)

介绍

上线的app中如果出现了bug,即使是一个非常小的bug,不及时更新的话有可能存在风险,若要及时更新就得将app重新打包发布到应用市场后,让用户再一次下载,这样就大大降低了用户体验,当热修复出现之后,这样的问题就不再是问题了。

  • 主要的热修复方案大致分为两派:
    阿里系:DeXposed、andfix:从底层二进制入手(c语言)。
    腾讯系:tinker:从java加载机制入手。

  • 热修复框架:AndFix、Tinker、Dexposed和Nuwa等等。

使用

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="10dp"
        android:onClick="fix"
        android:text="修复!"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent" />
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:onClick="startClick"
        android:text="start" />
</android.support.constraint.ConstraintLayout>
public class MainActivity extends AppCompatActivity {
    // “修复”按钮的点击事件
    public void fix(View view) {
        File file = FixDexUtils.loadFixedDex(this, Environment.getExternalStorageDirectory());
        if (file.exists()) {
            //修复bug
            Toast.makeText(this, "修复成功", Toast.LENGTH_SHORT).show();
        }
    }

    public void startClick(View view) {
        SimpleHotFixBugTest test = new SimpleHotFixBugTest();
        test.getBug(this);
    }
}
public class SimpleHotFixBugTest {
    public void getBug(Context context) {
        int i = 10;
        int a = 0;//bug除数不能为零
        Toast.makeText(context, "Hello,I am CSDN_LQR:" + i / a, Toast.LENGTH_SHORT).show();
    }
}

修复——加载dex格式补丁

/*加载dex格式补丁*/
/*描述 热修复工具(只认后缀是dex、apk、jar、zip的补丁)*/
public class FixDexUtils {
    private static final String DEX_SUFFIX = ".dex";
    private static final String APK_SUFFIX = ".apk";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    public static final String DEX_DIR = "odex";
    private static final String OPTIMIZE_DEX_DIR = "optimize_dex";
    private static HashSet<File> loadedDex = new HashSet<>();

    static {
        loadedDex.clear();
    }

    /**
     * 加载补丁,使用默认目录:data/data/包名/files/odex
     *
     * @param context
     */
    public static void loadFixedDex(Context context) {
        loadFixedDex(context, null);
    }

    /**
     * 加载补丁
     *  @param context       上下文
     * @param patchFilesDir 补丁所在目录
     */
    public static File loadFixedDex(Context context, File patchFilesDir) {
        if (context == null) {
            return patchFilesDir;
        }
        // 遍历所有的修复dex
        File fileDir = patchFilesDir != null ? patchFilesDir : new File(context.getFilesDir(), DEX_DIR);
        // data/data/包名/files/odex(这个可以任意位置)
        File[] listFiles = fileDir.listFiles();
        for (File file : listFiles) {
            if (file.getName().startsWith("classes") &&
                    (file.getName().endsWith(DEX_SUFFIX)
                            || file.getName().endsWith(APK_SUFFIX)
                            || file.getName().endsWith(JAR_SUFFIX)
                            || file.getName().endsWith(ZIP_SUFFIX))) {
                loadedDex.add(file);// 存入集合
            }
        }
        // dex合并之前的dex
        doDexInject(context, loadedDex);
        return patchFilesDir;
    }

    private static void doDexInject(Context appContext, HashSet<File> loadedDex) {
        String optimizeDir = appContext.getFilesDir().getAbsolutePath() + File.separator + OPTIMIZE_DEX_DIR;// data/data/包名/files/optimize_dex(这个必须是自己程序下的目录)
        File fopt = new File(optimizeDir);
        if (!fopt.exists()) {
            fopt.mkdirs();
        }
        try {
            // 1.加载应用程序的dex
            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
            for (File dex : loadedDex) {
                // 2.加载指定的修复的dex文件
                DexClassLoader dexLoader = new DexClassLoader(
                        dex.getAbsolutePath(),// 修复好的dex(补丁)所在目录
                        fopt.getAbsolutePath(),// 存放dex的解压目录(用于jar、zip、apk格式的补丁)
                        null,// 加载dex时需要的库
                        pathLoader// 父类加载器
                );
                // 3.合并
                Object dexPathList = getPathList(dexLoader);
                Object pathPathList = getPathList(pathLoader);
                Object leftDexElements = getDexElements(dexPathList);
                Object rightDexElements = getDexElements(pathPathList);
                // 合并完成
                Object dexElements = combineArray(leftDexElements, rightDexElements);
                // 重写给PathList里面的Element[] dexElements;赋值
                Object pathList = getPathList(pathLoader);// 一定要重新获取,不要用pathPathList,会报错
                setField(pathList, pathList.getClass(), "dexElements", dexElements);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 反射给对象中的属性重新赋值
     */
    private static void setField(Object obj, Class<?> cl, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cl.getDeclaredField(field);
        declaredField.setAccessible(true);
        declaredField.set(obj, value);
    }

    /**
     * 反射得到对象中的属性值
     */
    private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }


    /**
     * 反射得到类加载器中的pathList对象
     */
    private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
     * 反射得到pathList中的dexElements
     */
    private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {
        return getField(pathList, pathList.getClass(), "dexElements");
    }

    /**
     * 数组合并
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> componentType = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);// 得到左数组长度(补丁数组)
        int j = Array.getLength(arrayRhs);// 得到原dex数组长度
        int k = i + j;// 得到总数组长度(补丁数组+原dex数组)
        Object result = Array.newInstance(componentType, k);// 创建一个类型为componentType,长度为k的新数组
        System.arraycopy(arrayLhs, 0, result, 0, i);
        System.arraycopy(arrayRhs, 0, result, i, j);
        return result;
    }
}

加入权限

<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- 外部存储读权限,调试工具加载本地补丁需要 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>



<!-- 读取手机信息权限 -->

<uses-permission android:name="android.permission.READ_PHONE_STATE" />

修改后的SimpleHotFixBugTest.class

public class SimpleHotFixBugTest {
    public void getBug(Context context) {
        int i = 10;
        int a = 1;
        Toast.makeText(context, "Hello,I am CSDN_LQR:" + i / a, Toast.LENGTH_SHORT).show();
    }
}

在修复bug之后,可以使用Android Studio的Rebuild Project功能将代码进行编译,然后从build目录下找到对应的class文件。

在这里插入图片描述
将修复好的class文件复制到其他地方,例如桌面上的dex文件夹中。需要注意的是,在复制这个class文件时,需要把它所在的完整包目录一起复制。假设上图中修复好的class文件是SimpleHotFixBugTest.class,则到时复制出来的目录结构是:
在这里插入图片描述

将class文件打包成dex文件

  • dx指令的使用
  1. 配置环境变量(添加到classpath),然后命令行窗口(终端)可以在任意位置使用。
  2. 不配环境变量,直接在build-tools/安卓版本 目录下使用命令行窗口(终端)使用。
    在这里插入图片描述第一种方式参考java环境变量配置即可,这里我选用第二种方式,在上面显示的文件夹目录的空白处,按住shift+鼠标右击,可出现“在此处打开命令行窗口”。
    下面我们需要用到的命令是:
    dx –dex –output=dex文件完整路径 (空格) 要打包的完整class文件所在目录,如:
dx --dex --output=C:\Users\IT-Android\Desktop\dex\classes2.dex  C:\Users\IT-Android\Desktop\dex

使用补丁

将生成后的classes2.dex文件贴到手机sd中
在这里插入图片描述
放到:sdcard中
在这里插入图片描述

参考:https://blog.csdn.net/csdn_lqr/article/details/78534065

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值