Android 使用类加载器原理实现热修复

Posted 碎格子

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 使用类加载器原理实现热修复相关的知识,希望对你有一定的参考价值。

本篇热修复的原理是通过类加载器加载修复好的class文件来实现。

源码分析

项目在编译的时候会将java文件翻译成class文件,而class类在程序安装的时候会打包成dex文件,android 通过dalvik虚拟机运行dex文件,从而实现类的加载。

所以我们先来看看这里会用到两个类加载器:DexClassLoader和PathClassLoader。

public class DexClassLoader extends BaseDexClassLoader 
        /**
         * DexClassLoader用于找到被翻译过的和本地代码
         * 翻译过的类放置在Dex集合文件中,Dex集合被包含在Jar或apk的压缩文件中
         * <p>
         *
         * @param dexPath            含有classes和资源文件的Jar或者Apk文件列表,使用":"分割
         * @param optimizedDirectory 解压后dex的文件放置的目录,不可为空
         * @param libraryPath        包含本地库的列表目录,可为空
         * @param parent             父加载类
         */
        public DexClassLoader(String dexPath, String optimizedDirectory,
                              String libraryPath, ClassLoader parent) 
            super(dexPath, new File(optimizedDirectory), libraryPath, parent);
        
    


    public class PathClassLoader extends BaseDexClassLoader 
        /**
         * @param dexPath 含有classes和资源文件的Jar或者Apk文件列表,使用":"分割
         * @param parent  父加载类
         */
        public PathClassLoader(String dexPath, ClassLoader parent) 
            super(dexPath, null, null, parent);
        

        /**
         * @param dexPath     含有classes和资源文件的Jar或者Apk文件列表,使用":"分割
         * @param libraryPath 包含本地库的列表目录,可为空
         * @param parent      父加载类
         */
        public PathClassLoader(String dexPath, String libraryPath,
                               ClassLoader parent) 
            super(dexPath, null, libraryPath, parent);
        
        

可以看出这两个类都是BaseDexClassLoader的子类,都在构造方法里调用了父类构造方法。而代码上最大的区别在于DexClassLoader会传入zip/jar/apk文件解压后的放置的目录,而PathClassLoader传的是null。

来看看父类BaseDexClassLoader里做了什么:

public class BaseDexClassLoader extends ClassLoader 
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                              String libraryPath, ClassLoader parent) 
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException 
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        ...        
        return c;
    

    ...

这里最主要的是两个方法,一个构造方法,一个findClass的方法,构造方法主要创建了一个DexPathList对象,而findClass方法主要是调用DexPathList对象的findClass方法

那再来看看DexPathList的代码

final class DexPathList 
    private static final String DEX_SUFFIX = ".dex";
    private final Element[] dexElements;
    ...

    public DexPathList(ClassLoader definingContext, String dexPath,
                       String libraryPath, File optimizedDirectory) 
        ...
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                suppressedExceptions);
        ...
    


    /**
     * Makes an array of dex/resource path elements, one per element of
     * the given array.
     */
    private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                             ArrayList<IOException> suppressedExceptions) 
        ArrayList<Element> elements = new ArrayList<Element>();
        for (File file : files) 
            File zip = null;
            DexFile dex = null;
            String name = file.getName();

            if (file.isDirectory()) 
                // We support directories for looking up resources.
                // This is only useful for running libcore tests.
                elements.add(new Element(file, true, null, null));
             else if (file.isFile()) 
                if (name.endsWith(DEX_SUFFIX)) 
                    // Raw dex file (not inside a zip/jar).
                    try 
                        dex = loadDexFile(file, optimizedDirectory);
                     catch (IOException ex) 
                        System.logE("Unable to load dex file: " + file, ex);
                    
                 else 
                    zip = file;

                    try 
                        dex = loadDexFile(file, optimizedDirectory);
                     catch (IOException suppressed) 
                        suppressedExceptions.add(suppressed);
                    
                
             else 
                System.logW("ClassLoader referenced unknown path: " + file);
            

            if ((zip != null) || (dex != null)) 
                elements.add(new Element(file, false, zip, dex));
            
        

        return elements.toArray(new Element[elements.size()]);
    

    ...

    public Class findClass(String name, List<Throwable> suppressed) 
        for (Element element : dexElements) 
            DexFile dex = element.dexFile;

            if (dex != null) 
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) 
                    return clazz;
                
            
        
        ...
        return null;
    

重点来了,dexPath是由多个dex路径以”:”为分隔的一串字符串(如”/data/dexdir1:/data/dexdir2:…”),DexPathList调用makeDexElements时,dexPath被分割成多个dex文件添加进一个列表,然后作为参数传入makeDexElements方法。makeDexElements方法中使用for循环将每个dex文件构造一个Element对象,然后将构造出的Element对象都加到一个elements数组里,因此dex文件和Element是一对一的关系。而findClass则是从由makeDexElements方法返回的Element数组中取出dex文件,然后将dex中和name匹配的class返回。

思路

总结下来,DexClassLoader可以加载任意目录下的dex/jar/apk/zip文件,PathClassLoader加载的是系统/data/app目录下的apk文件。所以我们的思路就是将我们修复的补丁(dex文件)放置到Storage目录下,用DexClassLoader加载补丁文件,然后用PathClassLoader加载app的dex文件,接着我们分别把补丁dex对应的Element列表和app dex对应的Element列表取出来,将补丁的Element列表合并到app Element列表前面,从而使系统在从Element列表中取dex时先取到我们的补丁,这样来进行热修复。

实践

原理和思路都掌握了,就实际操作一下,这里只讲解mac下的操作

  1. 将bug修复后在Android Studio上Rebuild Project

  2. 找到app/build/intermediates/classes/debug/包名/XXX.class
    连带路径”包名/XXX.class”复制到桌面新建的dex文件夹下,这个.class文件对应修复的.java文件

注意要连带包名路径一起复制


3. 在终端输入

dx --dex --output=/Users/bixia/Desktop/dex/classes2.dex /Users/bixia/Desktop/dex

将生成的dex文件copy到手机sd目录下

4.编写加载的Utils类

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 String OPTIMIZE_DEX_DIR = "optimize_dex";
    private static HashSet<File> loadedDex = new HashSet<>();

    static 
        loadedDex.clear();
    

    /**
     * @param context
     */
    public static void loadFixedDex(Context context) 
        loadFixedDex(context, null);
    

    /**
     * 加载补丁
     *
     * @param context       上下文
     * @param patchFilesDir 补丁所在目录
     */
    public static void loadFixedDex(Context context, File patchFilesDir) 
        if (context == null) 
            return;
        
        // 遍历所有的修复dex
        File fileDir = patchFilesDir != null ? patchFilesDir : new File(Environment.getExternalStorageDirectory().getPath());//这里写dex文件放置的路径,我放置到SDStorage下
        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, loadedDex);
    

    private static void doDexInject(Context appContext, HashSet<File> loadedDex) 
        String optimizeDir = appContext.getFilesDir().getAbsolutePath() + File.separator + OPTIMIZE_DEX_DIR;
        File fopt = new File(optimizeDir);
        if (!fopt.exists()) 
            fopt.mkdirs();
        
        try 
            // 加载app的dex
            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
            for (File dex : loadedDex) 
                // 加载我们修复的dex文件
                DexClassLoader dexLoader = new DexClassLoader(
                        dex.getAbsolutePath(),// 修复好的dex(补丁)所在目录
                        fopt.getAbsolutePath(),// 存放dex的解压目录(用于jar、zip、apk格式的补丁)
                        null,// 加载dex时需要的库
                        pathLoader// 父类加载器
                );
                // 取出补丁dex列表和app dex列表对应的Element列表
                Object dexPathList = getPathList(dexLoader);
                Object pathPathList = getPathList(pathLoader);
                Object leftDexElements = getDexElements(dexPathList);
                Object rightDexElements = getDexElements(pathPathList);
                // 将两个Element列表合并
                Object dexElements = combineArray(leftDexElements, rightDexElements);
                // 重写给PathList里面的Element[] dexElements;赋值
//                Object pathList = getPathList(pathLoader);//这里有人遇到过使用pathPathList会报错的问题,但楼主没有遇到,有这样问题的童鞋可以把此行注释打开
                    // 重新给app的dex对应的Element列表赋上我们合并后的element列表值
                setField(pathPathList, pathPathList.getClass(), "dexElements", dexElements);
            
         catch (Exception e) 
            e.printStackTrace();
        
    

    /**
     * 反射给对象中的属性重新赋值
     */
    private static void setField(Object obj, Class<?> cl, String field, Object value) throws NoSuchFieldException, IllegalAccessException 
        Field declaredField = cl.getDeclaredField(field);
        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 arrayLhs, Object arrayRhs) 
        Class<?> componentType = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);// 得到左数组长度(补丁数组)
        int j = Array.getLength(arrayRhs);// 得到原dex数组长度
        int k = i + j;// 得到总数组长度(补丁数组+原dex数组)
        Object result = Array.newInstance(componentType, k);// 创建一个类型为componentType,长度为k的新数组
        System.arraycopy(arrayLhs, 0, result, 0, i);
        System.arraycopy(arrayRhs, 0, result, i, j);
        return result;
    

可能的错误

在执行dx命令时可能会报下面的错
error:

It appears you do not have 'build-tools-23.0.1' installed.
Use the 'android' tool to install them: 
    android update sdk --no-ui --filter 'build-tools-23.0.1'

根据提示执行:

android update sdk --no-ui --filter 'build-tools-23.0.1'

依旧报错

Error: Ignoring unknown package filter 'build-tools-23.0.1'
Warning: The package filter removed all packages. There is nothing to install.
         Please consider trying to update again without a package filter.

解决办法是将build-tools-23.0.1对应的id找到单独升级:

android list sdk --all
android update sdk -u -a -t <build-tools-23.0.1 id>

最后再执行,就会成功生成dex文件到桌面上的dex文件夹下

dx --dex --output=/Users/bixia/Desktop/dex/classes2.dex /Users/bixia/Desktop/dex

测试

本篇我是将LoginActivity中的一个按钮置空,然后在进行setOnClickListener的时候就会崩溃,我们将修复的补丁放到Storage下后,在App跳转LoginActivity之前调用Utils里的loadFixedDex方法,将补丁加载进来就会成功实现热修复。
此处我是直接在Application初始化的时候进行补丁的加载:

public class VipApplication extends Application 
    static DataHelper dataHelper;

    @Override
    public void onCreate() 
        super.onCreate();
        ...
        FixDexUtils.loadFixedDex(this);
    
    ...

标题

以上是关于Android 使用类加载器原理实现热修复的主要内容,如果未能解决你的问题,请参考以下文章

Android类加载器和热修复原理

Android类加载器和热修复原理

MultiDex与热修复实现原理

Android热修复:以DexClassLoader类加载原理编写demo实现类替换修复

Android热修复:以DexClassLoader类加载原理编写demo实现类替换修复

Android 代码热修复详解