热修复/热更新

热修复/热更新

  • 一.Android热修复
  • 二.热修复框架
  • 三.类加载器
    • 0.BootClassLoader
    • 1.PathClassLoader
    • 2.DexClassLoader
  • 四.实现思路
  • 五.代码
    • 1.FixManager
    • 2.App
    • 3.更加标准的代码
  • 五.制作补丁包
    • 1.写段有bug的工具类,并写个点击按钮调用
    • 2.运行项目到模拟器上
    • 3.修复ToastUtils工具类,本地测试没问题,build项目,并将ToastUtils.class文件拷贝出来
    • 4.创建dex/com/bawei/myfix文件夹,里面只有修复好的ToastUtils.class文件
    • 5.生成dex补丁文件
    • 6.将补丁文件放在对应的SD卡目录下进行修复,注意读写SD卡权限

一.Android热修复

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

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

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

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

二.热修复框架

热修复/热更新_第2张图片
我们主要使用tinker

三.类加载器

0.BootClassLoader

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

1.PathClassLoader

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

2.DexClassLoader

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

热修复/热更新_第3张图片
类加载器的时序图
热修复/热更新_第4张图片

四.实现思路

热修复/热更新_第5张图片
热修复/热更新_第6张图片

五.代码

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文件拷贝出来

热修复/热更新_第7张图片

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

5.生成dex补丁文件

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

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

你可能感兴趣的:(专高6,android,android,studio,gradle)