热修复/热更新

一.Android热修复

热修复,就是对线上版本的静默更新。当APP发布上线之后,如果出现了严重的bug,通常需要重新发版来修复,但是重新走发布流程可能时间比较长,重新安装APP用户体验也不友好,所以出现了热修复,热修复就是通过发布一个插件,使APP运行的时候加载插件里面的代码,从而解决缺陷,并且对用户来说是无感的(有时候可能需要重启一下APP)。

热修复的实现方案,一种是类加载方案,即dex插桩,这种思路在插件化中也会用到;还有一种是底层替换方案,即修改替换ArtMethod。采用类加载方案的主要是以腾讯系为主,包括微信的Tinker、qq空间的QZone、美团的Robust、饿了么的Amigo;采用底层替换方案的主要是阿里系的AndFix等。

热修复包括3部分:开发端、服务端和用户端。在开发端,通过Gradle插件生成补丁包,并上传到云端,客户端通过判断是否需要下载新的补丁包,并执行热修复。
在这里插入图片描述

(1)无需重新发版。
(2)快速修复线上bug,修复成功率高,降低损失。
(3)用户无感知修复,无需下载最新的应用,代价小。

二.热修复框架

在这里插入图片描述
我们主要使用tinker

三.类加载器

0.BootClassLoader

系统类加载器,当系统启动的时候加载常用类

1.PathClassLoader

加载应用中的类,只能加载已经安装到 Android 系统中的 APK 文件。因此不符合插件化的需求,不作考虑。

2.DexClassLoader

支持加载外部的 APK、Jar 或者 dex 文件,正好符合文件化的需求,所有的插件化方案都是使用 DexClassloader 来加载插件 APK 中的 .class文件的

在这里插入图片描述
类加载器的时序图
在这里插入图片描述

四.实现思路

在这里插入图片描述
在这里插入图片描述

五.代码

1.FixManager

public class FixManager {
    private Context mContext;
    private FixManager(Context context){
        mContext = context;
    }
    private static FixManager manager;
    public static FixManager getInstance(Context context){
        if(manager == null){
            synchronized (FixManager.class){
                if(manager == null){
                    manager = new FixManager(context);
                }
            }
        }
        return manager;
    }


    public void loadFixClass() throws NoSuchFieldException, IllegalAccessException {
        //1.反射机制获得补丁包的dexElements:DexClassLoader
        //1.0 准备补丁包的路径
        String patchPath = mContext.getExternalFilesDir(null).getAbsolutePath()+"/output.dex";//补丁包的SD卡路径,
        String cachePatchPath = mContext.getDir("patch",Context.MODE_PRIVATE).getAbsolutePath();//补丁包的缓存路径
        //1.1 将补丁到的dex文件加载到虚拟机内存中
        DexClassLoader dexClassLoader = new DexClassLoader(patchPath,cachePatchPath,null,mContext.getClassLoader());
        Class<?> superclass = dexClassLoader.getClass().getSuperclass();//获得BaseDexClassLoader class对象
        Field pathListField = superclass.getDeclaredField("pathList");//获得BaseDexClassLoader类成员属性
        pathListField.setAccessible(true);//属性的私有的需要暴力访问
        Object pathListObject = pathListField.get(dexClassLoader);//获得dexClassLoader对象的pathList属性值
        Field dexElementsField = pathListObject.getClass().getDeclaredField("dexElements");//获得PathList类的成员属性
        dexElementsField.setAccessible(true);//属性的私有的需要暴力访问
        Object dexElementsObject = dexElementsField.get(pathListObject);//获得pathListObject对象dexElements属性值
        //2.反射机制获得宿主app的 dexElements
        ClassLoader pathClassLoader = mContext.getClassLoader();//获得PathClassLoader加载器对象
        Object myPathListField = pathListField.get(pathClassLoader);//获得PathClassLoader对象的pathList属性值
        Object myDexElementsObject = dexElementsField.get(myPathListField);//获得myPathListField对象dexElements属性值
        //3.将2个数组合并newDexElements:补丁包在前面,宿主在后面
        int fixLength = Array.getLength(dexElementsObject);//补丁包的长度
        int myLength = Array.getLength(myDexElementsObject);//宿主数组长度
        int newDexElementsLength = fixLength + myLength;//新数组的长度
        //新的数组 参数一:数组中元素的类型 参数二:长度
        Object newDexElements = Array.newInstance(dexElementsObject.getClass().getComponentType(), newDexElementsLength);
        for(int i = 0;i<newDexElementsLength;i++){
            if(i<fixLength){//放补丁包
                Object o = Array.get(dexElementsObject, i);
                Array.set(newDexElements,i,o);
            }else{
                Object o = Array.get(myDexElementsObject, i - fixLength);
                Array.set(newDexElements,i,o);
            }
        }
        //4.反射机制将newDexElements新数组给宿主app放回去
        dexElementsField.set(myPathListField,newDexElements);
    }
}

2.App

public class App extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        try {
            FixManager.getInstance(this).loadFixClass();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

3.更加标准的代码

package com.bawei.myfix;

import android.content.Context;

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

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

/**
 * @Author : yaotianxue
 * @Time : On 2023/6/8 18:26
 * @Description : FixDexUtils
 */
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";

    private static final HashSet<File> loadedDex = new HashSet<File>();

    //加载补丁,使用默认目录:data/data/包

    public static void loadFixedDex(Context context) {

        loadFixedDex(context, null);

    }

    //加载补丁包

    public static void loadFixedDex(Context context, File patchFilesDir) {

        if (context == null) {

            return;

        }

        // 遍历所有的修复dex

        File fileDir = patchFilesDir != null ? patchFilesDir : new File(context.getExternalCacheDir().getAbsolutePath()); // data/data/包名/cache(这个可以任意位置)

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

    }

    private static void doDexInject(Context appContext) {

        String optimizeDir = appContext.getFilesDir().getAbsolutePath();// data/data/包名/files (这个必须是自己程序下的目录)

        File fopt = new File(optimizeDir);

        if (!fopt.exists()) {

            fopt.mkdirs();

        }

        try {

            // 1.加载应用程序的dex

            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();

            for (File dex : FixDexUtils.loadedDex) {

                // 2.加载指定的修复的dex文件

                DexClassLoader dexLoader = new DexClassLoader(dex.getAbsolutePath(), fopt.getAbsolutePath(), null, 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);

            }

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

    //反射给对象中的属性重新赋值

    private static void setField(Object obj, Class<?> cl, Object value) throws NoSuchFieldException, IllegalAccessException {

        Field declaredField = cl.getDeclaredField( "dexElements");

        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 left, Object right) {

        Class<?> componentType = left.getClass().getComponentType();

        int i = Array.getLength(left);// 得到左数组长度(补丁数组)

        int j = Array.getLength(right);// 得到原dex数组长度

        int k = i + j;// 得到总数组长度(补丁数组+原dex数组)

        Object result = Array.newInstance( componentType, k);// 创建一个类型为componentType,长度为k的新数组

        System.arraycopy(left, 0, result, 0, i);

        System.arraycopy(right, 0, result, i, j);

        return result;

    }

}

五.制作补丁包

1.写段有bug的工具类,并写个点击按钮调用

public class ToastUtils {
    public static void toast(){
        int a = 1;
        int b = 0;
        int c = a/b;
    }
}

2.运行项目到模拟器上

3.修复ToastUtils工具类,本地测试没问题,build项目,并将ToastUtils.class文件拷贝出来

在这里插入图片描述

4.创建dex/com/bawei/myfix文件夹,里面只有修复好的ToastUtils.class文件

5.生成dex补丁文件

(1)Android SDK提供了dx.bat工具将class文件转成dex文件,目录如下:
在这里插入图片描述
(2)将第4步骤创建的dex文件夹放在sdk目录下,如上图所示
(3)cmd到SDK的路径,如上图所示
(4)执行命令:将dex文件夹里面的内容打成output.dex
.\dx --dex --output = .\output.dex .\dex

6.将补丁文件放在对应的SD卡目录下进行修复,注意读写SD卡权限

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值