Android热修复之Java层

2021-03-03  本文已影响0人  编程的猫

什么是热修复

当线上应用出现bug时,无需用户安装,推送补丁到用户端无感知修复bug,节省用户流量提高用户使用体验,修复准确率高

class类只会被ClasssLoader加载一次,Dalvik/ART加载dex文件。通过类加载器(ClassLoader)加载代码替换相关的代码。

  1. 在Native层替换方法表中的方法,直接在虚拟机的方法区实现替换
  2. 在Java层实现热修复
    前者是替换后立即生效,后者是需要重启App生效

Java的懒加载机制,在App不重新启动时,新类不能替换老的类。Class只能被ClassLoader加载一次,App启动后字节码文件已经被全部加载到虚拟机,所以Java层的热修复重新启动了App才会生效。

热修复的不足

所有的热修复不能保证100%修复成功

64K问题

Android虚拟机可执行的字节码单个dex文件内可引用的方法数量最大限制是65536,超过这个数量就会爆出异常。
解决方法是将应用构建流程配置为多个dex包

Dalvik 可执行文件分包配置

1.将 Gradle 构建配置更改为启用 Dalvik 可执行文件分包
2.修改清单文件以引用 MultiDexApplication 类
multiDexEnabled true

规避64K限制

合理的使用方法数量资源;在gradle中配置使用代码混淆和代码自检,去除无用的代码方法

Java层手写Android热修复
package com.example.firstapplication.hotfix;

import android.content.Context;
import android.widget.Toast;

/**
 * Create by pengQun 2021/3/3
 * Desc:创建一个bug类
 */
public class BugClass {

    public static void bugClass(Context context) {
        Toast.makeText(context, context.getPackageName() + ",这是一个Bug...", Toast.LENGTH_SHORT).show();
       
    }
}

package com.example.firstapplication.hotfix;

import android.content.Context;
import android.os.Environment;
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.NonNull;

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;

/**
 * Create by pengQun
 * Desc:热修复的核心工具类
 */
public class FixDexUtil {

    private static final String TAG = FixDexUtil.class.getSimpleName();
    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();
    }

    public static void loadFixedDex(Context context) throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
        loadFixedDex(context, null);
    }

    /**
     * 加载补丁
     *
     * @param context      上下文
     * @param patchFileDir 补丁所在的目录
     */
    public static void loadFixedDex(Context context, File patchFileDir) throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
        //合并dex(补丁dex合并现有的dex)
        doInjectDex(context, loadedDex);
    }

    /**
     * 验证是否需要热修复
     *
     * @param context context
     */
    public static boolean isGoingToFix(@NonNull Context context) {
        boolean canFix = false;
        File externalFilesDir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
        String downPath = externalFilesDir.getAbsolutePath();
        String path007 = downPath + File.separator + "007";
        Log.d(TAG, "path007: " + path007);
//        String pathDex = downPath + File.separator + DEX_DIR;

        File fileDir = new File(path007);
        if (!fileDir.exists()) {
            fileDir.mkdirs();
        }

        File[] listFiles = fileDir.listFiles();
        for (File listFile : listFiles) {
            String fileName = listFile.getName();
            if (fileName.startsWith("classes")
                    && (fileName.endsWith(DEX_SUFFIX) ||
                    fileName.endsWith(JAR_SUFFIX) ||
                    fileName.endsWith(ZIP_SUFFIX) ||
                    fileName.endsWith(APK_SUFFIX))) {
                loadedDex.add(listFile);
                canFix = true;
            }
        }
        return canFix;
    }

    private static void doInjectDex(Context context, HashSet<File> hashSet) throws IllegalAccessException, NoSuchFieldException, ClassNotFoundException {
        String optimizeDir = context.getFilesDir().getAbsolutePath()
                + File.separator + OPTIMIZE_DEX_DIR;
        Log.d(TAG, "------> optimizeDir: " + optimizeDir);
        //存放dex的解压目录
        File file = new File(optimizeDir);
        if (!file.exists()) {
            file.mkdirs();
        }

        //1.加载应用程序dex的classLoader
        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();

        //遍历修复的dex文件
        for (File dexFile : hashSet) {

            //2.加载指定修复dex的classLoader(补丁的classLoader)
            DexClassLoader dexClassLoader = new DexClassLoader(
                    dexFile.getAbsolutePath(),//修复补丁所在的目录
                    file.getAbsolutePath(),//补丁的解压目录(用于jar,zip,zpk格式的补丁)
                    null,//加载dex时需要的库
                    pathClassLoader//父类加载器
            );

            //3.获取dex,开始合并 合并的目标是Element[]
            // BaseDexClassLoader中有变量:DexPathList pathList
            // DexPathList中有变量:Element[] dexElements

            //3.1获取pathList

            //获取补丁中的pathList
            Object dexPathList = getPathList(dexClassLoader);
            //获取当前apk的dex中的pathList
            Object apkPathList = getPathList(pathClassLoader);

            //3.2反射获取element数组

            //获取补丁中的element数组
            Object dexElements = getDexElements(dexPathList);
            //获取apk的dex中的element数组
            Object apkDexElements = getDexElements(apkPathList);

            //4.合并Element数组
            Object combineArray = combineArray(dexElements, apkDexElements);

            //5.重新给PathList里边的Element[] dexElements 赋值

            //一定要重新获取,不要用上面的apkPathList,会报错
            Object pathList = getPathList(pathClassLoader);
            setField(pathList, pathList.getClass(), "dexElements", combineArray);
        }
        Toast.makeText(context, "修复完成", Toast.LENGTH_SHORT).show();

    }

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

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

    /**
     * 反射得到类加载器中的pathList对象
     *
     * @param baseDexClassLoader
     * @return
     */
    private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        Class<?> aClass = Class.forName("dalvik.system.BaseDexClassLoader");
        return getField(baseDexClassLoader, aClass, "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);
        //得到原数组dex的长度
        int j = Array.getLength(arrayRhs);
        //数组的总长度
        int length = i + j;
        //创建一个类型为class的数组
        Object instance = Array.newInstance(componentType, length);
        //复制数组到目标数组
        System.arraycopy(arrayLhs, 0, instance, 0, i);
        System.arraycopy(arrayRhs, 0, instance, i, j);
        return instance;
    }
}

运行代码点击按钮调用 BugClass.bugClass() 方法弹出bug提示
将BugClass代码修正,然后Build-----> ReBuild Project重新构建项目生成字节码文件。如下:

public class BugClass {

    public static void bugClass(Context context) {
        Toast.makeText(context, "bug已经修复", Toast.LENGTH_SHORT).show();
    }
}

复制ReBuild Project后的BugClass.class字节码文件,路径如下:


hotfix.png

dex打包,可参考我的另一篇博文
mac下dex打包

效果

看下修复前和修复后的效果


fix_before.jpg
fix_after.jpg

总结

java层热修复是利用classLoader只能加载一次class类到Dalvik/ART虚拟机执行的特点,将修复后的.class文件打包成.dex文件在问题代码前预先加载到虚拟机,以此达到热修复的目的。(在dex的Element数组合并的时候就能看出来,补丁的Element数组要放置在apk的Element的前边,ClassLoader加载了补丁的class就不会再去加载apk中的class)

上一篇 下一篇

猜你喜欢

热点阅读