美文网首页
Android热修复之Java层

Android热修复之Java层

作者: 编程的猫 | 来源:发表于2021-03-03 20:17 被阅读0次

    什么是热修复

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

    • 热修复的原理

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

    • 热修复分为两种:
    1. 在Native层替换方法表中的方法,直接在虚拟机的方法区实现替换
    2. 在Java层实现热修复
      前者是替换后立即生效,后者是需要重启App生效
    • 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热修复
    • 创建一个BugClass类
    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
    • 将BugClass.class文件使用dex工具打包成dex文件

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

    • 将生成好的BugClass.dex文件拷贝放到手机的文件路径下(/storage/emulated/0/Android/data/com.example.firstapplication/files/Download/007)【该路径要与代码中的对应】,然后重启App

    效果

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


    fix_before.jpg
    fix_after.jpg

    总结

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

    相关文章

      网友评论

          本文标题:Android热修复之Java层

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