Android热补丁动态修复技术(一):从Dex分包原理到热补丁

一、参考

博文:安卓App热补丁动态修复技术介绍——by QQ空间终端开发团队
博文:Android dex分包方案——by 猫的午后
开源项目:https://github.com/jasonross/Nuwa
开源项目:https://github.com/dodola/HotFix
感谢以上几位大神分享的技术知识!

关于热补丁技术,以上文章已经做了很详细的描述。但是细节上的东西都一带而过,这里会做出更为详细的说明,更适合初学者学习这门技术。

二、Dex分包方案的由来

2.1 Dalvik限制

众所周知,当apk解压后里面是只有一个classes.dex文件的,而这个dex文件里面就包含了我们项目的所有.class文件。

但是当一个app功能越来越复杂,可能会出现两个问题:

  1. 编译失败,因为一个dvm中存储方法id用的是short类型,导致dex中方法不能超过65536个
  2. 你的apk在android 2.3之前的机器无法安装,因为dex文件过大(用来执行dexopt的内存只分配了5M)

2.2 解决方案

针对上述两个问题,有人研究出了dex分包方案。
原理就是将编译好的class文件拆分打包成两个dex,绕过dex方法数量的限制以及安装时的检查,在运行时再动态加载第二个dex文件中。

除了第一个dex文件(即正常apk包唯一包含的Dex文件),其它dex文件都以资源的方式放在安装包中,并在Application的onCreate回调中被注入到系统的ClassLoader。因此,对于那些在注入之前已经引用到的类(以及它们所在的jar),必须放入第一个Dex文件中。

三、Dex分包的原理——ClassLoader

接下来我们就来看看,如何将第二个dex文件注入到系统中。

3.1 ClassLoader体系

我们都知道,java执行程序的时候是需要先将字节码加载到jvm之后才会被执行的,而这个过程就是使用到了ClassLoader类加载器。Android也是如此

以下是DVM的ClassLoader体系

这里写图片描述

查看官方文档可以知道以下两点:
1.Android系统是通过PathClassLoader加载系统类和已安装的应用的。
Android uses this class for its system class loader and for its application class loader(s).

2.而DexClassPath则可以从一个jar包或者未安装的apk中加载dex
A class loader that loads classes from .jar and .apk files containing a classes.dex entry. This can be used to execute code not installed as part of an application.

从这里就可以看出,动态加载dex的时候我们应该使用DexClassLoader

3.2 ClassLoader源码分析

源码可以到这个网站查阅:http://androidxref.com/

DexClassLoader和PathClassLoader都只重写了BaseDexClassLoader的构造而已,而具体的加载逻辑则在BaseDexClassLoader中。

这部分源码都很简单,请务必看懂

BaseDexClassLoader部分源码

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    /**
     * 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 libraryPath 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 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);
        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;
    }
}

从源码得知,当我们需要加载一个class时,实际是从pathList中去找的,而pathList则是DexPathList的一个实体。

DexPathList部分源码:

/*package*/ final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    private static final String APK_SUFFIX = ".apk";

    /** class definition context */
    private final ClassLoader definingContext;

    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private final Element[] dexElements;

    /**
     * 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是用来保存dex的数组,而每个dex文件其实就是DexFile对象。遍历dexElements,然后通过DexFile去加载class文件,加载成功就返回,否则返回null

通常情况下,dexElements数组中只会有一个元素,就是apk安装包中的classes.dex
而我们则可以通过反射,强行的将一个外部的dex文件添加到此dexElements中,这就是dex的分包原理了。
这也是热补丁修复技术的原理。

四、热补丁修复技术的原理

上面的源码,我们注意到一点,如果两个dex中存在相同的class文件会怎样?
先从第一个dex中找,找到了直接返回,遍历结束。而第二个dex中的class永远不会被加载进来。
简而言之,两个dex中存在相同class的情况下,dex1的class会覆盖dex2的class。
盗一下QQ空间的图,如图:classes1.dex中的Qzone.class并不会被加载
这里写图片描述

而热补丁技术则利用了这一特性,当一个app出现bug的时候,我们就可以将出现那个bug的类修复后,重新编译打包成dex,插入到dexElements的前面,那么出现bug的类就会被覆盖,app正常运行,这就是热修复的原理了。
这里写图片描述

五、本章结束

这章为大家介绍了热补丁技术的原理,但是大家可能并不会实际操作。
1. 怎么通过反射将dex插入到elements
2. 怎么讲修复后的类打包成dex
这将是下一篇博客的内容,感谢阅读。

阅读更多
版权声明:本文为博主原创文章,转载请声明出处http://blog.csdn.net/u010386612 https://blog.csdn.net/u010386612/article/details/50885320
文章标签: android 补丁 热补丁
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭