性能优化——Android热修复技术,类加载机制详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013928412/article/details/80428476
一、背景
热修复技术慢慢的成为Android开发必不可少的技术,也是成为一名高级程序员必不可少的技能之一。那么什么是热修复技术呢?
当app上线之后,发现了一个严重的bug,需要紧急修复,按照以往的惯例是修复bug、打包测试、发布到各个应用市场、用户覆盖安装,这样不仅耗时耗力,而且也很影响用户的体验。那么有没有一种技术可以在不升级app版本的情况下,直接就将bug修复呢?热修复技术就应运而生。
目前比较火的热修复方案主要有:
1、阿里系:AndFix ,从native层进行修复
2、腾讯系:代表有Tinker,从Java的类加载机制入手
热修复技术需要解决的问题有类替换,资源替换,dalvik与art虚拟机的兼容,阿里系还需要处理不同版本系统的兼容等。本篇主要介绍热修复技术中的类加载机制与实现

二、类加载机制
在Android中,class文件最终会被优化成dex文件,并通过PathClassLoader和DexClassLoader来加载
接下来分析一下PathClassLoader和DexClassLoader源码文件,来看看Android是如何加载类文件的,涉及到的文件(7.0.0系统): PathClassLoader DexClassLoader BaseDexClassLoader DexPathList
首先看看PathClassLoader和DexClassLoader的源码:
public class DexClassLoader extends BaseDexClassLoader {

    public DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath,new File(optimizedDirectory),librarySearchPath,parent);
    }
}
public class PathClassLoader extends BaseDexClassLoader {

    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}
D exClassLoader和PathClassLoader的代码很简单,都是继承自BaseDexClassLoader,并且构造方法也同样都调用了父类的构造方法,区别就是第二个参数,DexClassLoader有优化目录参数optimizedDirectory,而PathClassLoader的参数为null
也正是因为这点,导致DexClassLoader和PathClassLoader有不同的使用场景:DexClassLoader可以用于加载任意目录下的dex、zip、jar、apk里面的dex文件,而PathClassLoader只能加载应用路径下的目录(/data/data/app目录)
1、构造方法
既然都是继承自BaseDexClassLoader,那么我们看看BaseDexClassLoader的构造方法都做了哪些事:
/**
 * Constructs an instance.
 *
 * @param dexPath the list of jar/apk files containing classes and
 * resources, delimited by {@code File.pathSeparator}, which
 * defaults to {@code ":"} on Android
 * @param optimizedDirectory directory where optimized dex files
 * should be written; may be {@code null}
 * @param librarySearchPath the list of directories containing native
 * libraries, delimited by {@code File.pathSeparator}; may be
 * {@code null}
 * @param parent the parent class loader
 */
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
        String librarySearchPath, ClassLoader parent) {
    super(parent);
    this.pathList = new DexPathList(this, dexPath, librarySearchPath, optimizedDirectory);
}
我们先看看参数:
dexPath:含有类和资源的jar/apk的路径,多个路径默认会被冒号分开
optimizedDirectory:优化路径,放置优化后的dex文件的路径,app安装的时候会将dex文件先优化成odex文件才能被使用,此路径就是用于放置odex文件的路径,并且此路径只能是app的内部路径(data/data/包名/XXX)
librarySearchPath:加载类时需要使用的本地库
parent:父加载器
在构造方法中初始化了DexPathList,我们来看看DexPathList的构造方法:
public DexPathList(ClassLoader definingContext, String dexPath,
        String librarySearchPath, File optimizedDirectory) {
//省略部分代码
//赋值类加载器
    this.definingContext = definingContext;
    // 将dex文件或压缩包中的信息保存到dexElements中
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext);
   //省略部分代码
}


那么dexElements是什么呢?来看看声明的地方:
/**
* List of dex/resource (class path) elements.
* Should be called pathElements, but the Facebook app uses reflection
* to modify 'dexElements' (http://b/7726934).
*/
private Element[] dexElements;
从注释可以看出,dexElements实际上是个Element集合,再来看看makeDexElements方法是如何将信息保存到dexElements中的:
/**
 * Makes an array of dex/resource path elements, one per element of
 * the given array.
 */
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,List<IOException> suppressedExceptions,ClassLoader loader) {
    return makeElements(files,optimizedDirectory,suppressedExceptions,false,loader);
}
是个重载,继续看看makeElements方法:
private static Element[] makeElements(List<File> files, File optimizedDirectory,List<IOException> suppressedExceptions,boolean ignoreDexFiles,ClassLoader loader) {
//实例化dex文件或者包含dex文件的的文件长度的Element数组
    Element[] elements = new Element[files.size()];
    int elementsPos = 0;
    /*
     * Open all files and load the (direct or contained) dex files
     * up front.
     */
    for (File file : files) {
        File zip = null;
        File dir = new File("");
        DexFile dex = null;
        String path = file.getPath();
        String name = file.getName();

    //省略部分代码
//如果是dex文件
     if (name.endsWith(DEX_SUFFIX)) {
         dex = loadDexFile(file, optimizedDirectory, loader,elements); 
      } else {
//如果是那些包含dex文件的压缩文件
       zip = file;
dex = loadDexFile(file, optimizedDirectory, loader, elements);
            }
        } 
//省略部分代码
    return elements;
}


到这里,基本就可以明白,DexPathList的构造方法是将dex文件或者包含dex文件的压缩文件路径转化为一个个Element对象,然后保存到elements数组中
2、findClass
在PathClassLoader和DexClassLoader中是通过父类的BaseDexClassLoader中的findClass来将类加载进来的,我们看看源码中的实现:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
            cnfe.addSuppressed(t);
        }
        throw cnfe;
    }
    return c;
}


我们可以看到,给findClass方法传入类名,通过我们在BaseDexClassLoader实例化的pathList的findClass方法返回需要加载的类Class,那么我们继续跟进DexPathList:
/**
 * Finds the named class in one of the dex files pointed at by
 * this instance. This will find the one in the earliest listed
 * path element. If the class is found but has not yet been
 * defined, then this method will define it in the defining
 * context that this instance was constructed with.
 *
 * @param name of class to find
 * @param suppressed exceptions encountered whilst finding the class
 * @return the named class or {@code null} if the class is not
 * found in any of the dex files
 */
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;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}


从中我们可以看出,整体就是循环遍历dexElements,然后通过取出element的dexFile,并通过loadClassBinaryName方法返回所需要的类,不为空就直接返回,到这里,就终于看到了返回的类,这里也是热修复的关键:循环遍历含有dex路径的element,如果能从这个element中返回类,则直接返回,循环终止!
三、热修复的思路
通过上面一长串源码的分析研究,我们就可以得出,app的类加载器通过DexPathList循环遍历elements数组,并从遍历的Element对象中查找所需要的类,如果找到则直接返回。注意,从上面的源码分析来看,这里面的每个Element相当于是一个dex文件,那么假如我们的项目有个class有了bug,我们如果将修复好的class文件编译成dex文件,然后放到elements数组的前面,那么就会直接返回修复好的class,后面含有bug的class所在的element则不会遍历到,达到修复的目的,原理图如下
四、实践
前期准备:
1、由于Android studio的instant Run也使用了热修复的的原理,会对测试造成一定的影响,先关闭Android studio的instant run功能:
2、将修复的类的class文件编译成dex文件
将class文件打包成dex文件需要适用到Android自带的dx.bat工具

使用如下命令
dx --dex --output=生成的补丁的路径 要打包的完整class文件所在路径
dx --dex --output=E:\BlogCode\HotUpdateDemo\app\build\intermediates\classes\debug\patch.dex mfy\com\hotupdatedemo\TestUtil.class
将补丁放到手机里面,我放在了内存卡的根目录下
3、添加分包处理
在gradle里面multiDexEnabled true
自定义Application
public class MyApplication extends Application {
    @Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        MultiDex.install(base);
    }
}


加载补丁:
public static void fix(Context context, String patchPath) {
    if (context == null) return;
    File fileDir = context.getDir(Const.DEX_DIR, Context.MODE_PRIVATE);
    //创建优化目录
    if (!fileDir.exists()) {
        fileDir.mkdirs();
    }
    doInject(context,
            patchPath,//dex补丁所在的路径,可以任意路径
            fileDir,//app内部路径,存放优化dex优化文件的路径
            Const.PATCH_NAME);
}

private static void doInject(Context appContext, String patchPath, File fileDir, String patchName) {

    //获取应用内部的类加载器
    PathClassLoader pathClassLoader = (PathClassLoader) appContext.getClassLoader();
    //实例化dexClassLoader用于加载补丁dex
    DexClassLoader dexClassLoader = new DexClassLoader(patchPath, fileDir.getAbsolutePath(), null, pathClassLoader);
    try {
        //获取dexclassloader和pathclassloader的dexpathlist
        Object dexPathList = getPathList(dexClassLoader);
        Object pathPathList = getPathList(pathClassLoader);
        //获取补丁的elements数组
        Object dexElements = getDexElements(dexPathList);
        //获取程序的elements
        Object pathElements = getDexElements(pathPathList);
        //合并两个数组
        Object resultElements = combineArray(dexElements, pathElements);
        //将合并后的数组设置给PathClassLoader
        setField(pathPathList, pathPathList.getClass(), "dexElements", resultElements);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

}

private static void setField(Object pathPathList, Class<?> clazz, String fieldName, Object resultElements) {
    try {
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(pathPathList, resultElements);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}

private static Object combineArray(Object arrayLhs, Object arrayRhs) {
    Class<?> clazz = arrayLhs.getClass().getComponentType();
    int i = Array.getLength(arrayLhs);
    int j = Array.getLength(arrayRhs) + i;
    Object result = Array.newInstance(clazz, j);
    for (int k = 0; k < j; ++k) {
        if (k < i) {
            Array.set(result, k, Array.get(arrayLhs, k));
        } else {
            Array.set(result, k, Array.get(arrayRhs, k - i));
        }
    }
    return result;
}

//获得dexElements
private static Object getDexElements(Object dexPathList) {
    return getField(dexPathList, dexPathList.getClass(), "dexElements");
}

//获得DexPathList
private static Object getPathList(Object classLoader) throws ClassNotFoundException {
    return getField(classLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}

//通过反射获取一个类私有属性的值
private static Object getField(Object obj, Class<?> clazz, String fieldName) {
    try {
        Field field = clazz.getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(obj);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return null;
}

项目地址:

猜你喜欢

转载自blog.csdn.net/u013928412/article/details/80428476