热修复的原理与实现的demo

算是记录知识的博客,所以精简为主。

热修复分为两种:

Java层修复,以QZone,Tinker为代表。需要重新启动后才能完成修复。

Native层修复,以阿里系为代表。可以达成及时修复。

实现demo用的是Java层的修复。java层修复的原理是通过类加载器的机制来实现——双亲委派机制。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

 这上面是系统源码,可以看出在加载一个class的时候,他会先让父类去加载,父类如果可以加载,那就父类加载,父类加载不了,那就会抛出一个异常,让子类去加载。这么做的主要原因就是可以避免一个类被重复加载。所以你肯定遇到过使用第三方库,编译时提示你class重复这个问题。那么类替换的热修复方案就是,让fix类先被加载,这样有问题的class就不会被加载了

在5.0之前会有一个CLASS_ISPREVERIFIED的错误。这个错误的解释是:

  1. 在apk安装的时候,虚拟机会将dex优化成odex后才拿去执行。在这个过程中会对所有class一个校验。
  2. 校验方式:假设A该类在它的static方法,private方法,构造函数,override方法中直接引用到B类。如果A类和B类在同一个dex中,那么A类就会被打上CLASS_ISPREVERIFIED标记
  3. 被打上这个标记的类不能引用其他dex中的类,否则就会报图中的错误
  4. 在我们的Demo中,MainActivity和Cat本身是在同一个dex中的,所以MainActivity被打上了CLASS_ISPREVERIFIED。而我们修复bug的时候却引用了另外一个dex的Cat.class,所以这里就报错了
  5. 而普通分包方案则不会出现这个错误,因为引用和被引用的两个类一开始就不在同一个dex中,所以校验的时候并不会被打上CLASS_ISPREVERIFIED

上文是在其他博客中引用的。如果你的minSDKVersion大于5.0就不用考虑这件事儿了。上面解决方案是插桩,这里不做赘述。

首先需要把一个class变成dex,使用SDK下Tools自带的dx工具就行了。

dx --dex --output out.dex in.class

记住class需要放在相应的包名路径中。

得到了class之后,就用下面的工具类就行。


/**
 * 修复的工具类
 * on 2020/4/6
 */
public class FixUtil {

    /**
     * 讲dex存到自己的文件夹下
     * @param context
     * @param dirName 存放dex的目录
     * @param dexName dex名字
     */
    public static void loadDex(Context context,String dirName,String dexName) {
        //构建存放dex目录
        File dexDir = context.getDir(dirName, Context.MODE_PRIVATE);
        //拿到dex文件拷贝
        String dexPath = dexDir.getAbsoluteFile() + File.separator + dexName;
        File dexFile = new File(dexPath);
        if (!dexFile.exists()) {
            try {
                dexFile.createNewFile();
                FixUtil.copyFiles(context, dexName, dexFile);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
    
    
    
    public static void FixDex(Context context,File dexDir){
        //1.获得要修复的dexList
        List<File> dexList = getDexList(context, dexDir);

        try {
            //2.找到pathList对象
            Field pathListField = FixUtil.getField(context.getClassLoader(), "pathList");
            Object pathList = pathListField.get(context.getClassLoader());
            //3.获取系统的dexElements
            Field dexElementsField = FixUtil.getField(pathList, "dexElements");
            Object[] oldDexElements = (Object[]) dexElementsField.get(pathList);
            //4.获取makeDexElements
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            Method makeDexElements = FixUtil.getMethod(pathList, "makeDexElements", List.class, File.class
                    , List.class, ClassLoader.class);
            //5.加载修复的dexList
            Object[] fixDexElements = (Object[]) makeDexElements.invoke(pathList, dexList, dexDir, suppressedExceptions,
                    context.getClassLoader());
            //6.创建新的Elements数组
            Object[] newDexElements = (Object[]) Array.newInstance(oldDexElements.getClass().getComponentType()
                    , oldDexElements.length + fixDexElements.length);
            //7.合并Elements数组
            System.arraycopy(fixDexElements, 0, newDexElements, 0, fixDexElements.length);
            System.arraycopy(oldDexElements, 0, newDexElements, fixDexElements.length, fixDexElements.length);
            //8.将系统的Elements重新设置
            dexElementsField.set(pathList, newDexElements);
            Log.d("TAG", "修复完成");
        } catch (Exception e) {
            Log.d("TAG", "修复失败" + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 获取文件夹下所有.dex文件
     * @param context
     * @param dexDir
     * @return
     */
    public static List<File> getDexList(Context context, File dexDir) {
        File[] files = dexDir.listFiles();
        List<File> fileList = new ArrayList<>();
        for (File file : files) {
            if (file.getName().endsWith(".dex")) {
                fileList.add(file);
            }
        }

        return fileList;
    }


    /**
     * 将文件拷贝到私有目录下
     * @param context
     * @param fileName
     * @param desFile
     */
    public static void copyFiles(Context context, String fileName, File desFile) {
        InputStream in = null;
        OutputStream out = null;

        try {
            in = context.getAssets().open(fileName);
            out = new FileOutputStream(desFile.getAbsolutePath());
            byte[] bytes = new byte[1024];
            int len = 0;
            while ((len = in.read(bytes)) != -1)
                out.write(bytes, 0, len);
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (in != null)
                    in.close();
                if (out != null)
                    out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 反射属性
     * @param object
     * @param name
     * @return
     * @throws NoSuchFieldException
     */
    public static Field getField(Object object, String name) throws NoSuchFieldException {
        Class<?> aClass = object.getClass();
        while (aClass != null) {
            try {
                Field declaredField = aClass.getDeclaredField(name);
                if (!declaredField.isAccessible()) {
                    declaredField.setAccessible(true);
                }
                return declaredField;
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
                aClass = aClass.getSuperclass();
            }

        }
        throw new NoSuchFieldException("找不到对应属性");
    }

    /**
     * 反射方法
     * @param object
     * @param name
     * @param param
     * @return
     * @throws NoSuchMethodException
     */
    public static Method getMethod(Object object, String name, Class... param) throws NoSuchMethodException {
        Class<?> aClass = object.getClass();
        while (aClass != null) {
            try {
                Method declaredField = aClass.getDeclaredMethod(name, param);
                if (!declaredField.isAccessible()) {
                    declaredField.setAccessible(true);
                }
                return declaredField;
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
                aClass = aClass.getSuperclass();
            }

        }
        throw new NoSuchMethodException("找不到对应属性");
    }


}

我将dex存放在Asset文件夹下模拟的网络加载,如果是网络加载的话替换下这块就行。然后记住在Application里面去加载你存放dex文件夹下面的所有fix文件。为什么要重新启动才能加载?因为你没法去把一个已经加载的class给卸载咯。还有记住别开混淆!

发布了8 篇原创文章 · 获赞 4 · 访问量 4781

猜你喜欢

转载自blog.csdn.net/woailqy/article/details/105363578