Android 热修复 -- 实现原理

3,264 阅读7分钟
原文链接: ownwell.github.io

写在前面的话,8月30号在发表了这篇文章,本想给兄弟们分享,不过由于本人的前期技术调研的不够审慎,出现了众多低级错误之处如RN、热修复用于A/B Test等问题,大帅markzhai及时指出,在此谢过大帅markzhai,同时希望广大读者多提意见和建议,文中有错误或不妥之处直言提出。
本人在此确保自己以后写的东西能做够功课,宁可不写也不能混淆大家视听。
我不怕错误,我只怕我不能成长。

关于热修复其实很早都想动手写,不过由于没研究过具体的实践,不敢乱谈。
那么何为热修复呢?所谓热修复,无非是线上出了bug,开发人员可以发补丁,应用程序默默下载好对应问题的补丁,修复这个bug。这种热修复其实很适合client-server的模式,当然了客户端肯定也是适用的。

热修复/热部署 最早使用在web后端的,至于客户端热修复,最早听到热修复是2014年,当时在阿里技术嘉年华分享会上,阿里分享的插件化部署专题中讲到其中的热修复。反而在去年(2015年),出了很多热修复和插件化的框架,可以说是插件化/热部署元年。

虽然我们落后了半年,所以赶快补上热修复这一章。

为什么需要热修复

上周去百度,和鸿洋有过交流,当时,鸿洋大神说:热修复只能在国内玩,国外都是Google play。
的确,目前热修复尽管有很多坑,做了好多工作,可能吃力不讨好,各种适配可能还是没修复线上的有些Bug。不过呢,对于一个产品有热修复毕竟是件好事。尤其是对于一个有众多用户的app(如支付宝、微信、手淘等),一个bug不只是影响到几个几十个用户,一些创业公司的APP,崩溃或者bug可能直接导致用户卸载和永不使用,所以,就冲它有不用发版也可以解决我们线上的bug,我们的app也要适当考虑加入热修复。

在IOS上,有JsPatch、有waxPatch,有些游戏公司自己搞了一个lua引擎放到应用里,搞一些类似动态部署的东西。
也可以采用其他方案,如RN,阿里的Weex等方案。

热修复

热修复,这个词是在去年QQ空间开发团队,发表的一篇文章安卓App热补丁动态修复技术介绍出现后,在”江湖”上引起了”动荡。Android程序员奔走相告–“我们终于找到梦寐以求的实现热修复的理论支持”。

可能还有其他方案如阿里的And-Fix,ClassLoader的替代方案。参考文章下有and-fix和ClassLoader的文章,记得点开阅读 。其中 markzhai也提到了现在classLoader替换方案已经成这一年多来新的变化。

安卓App热补丁动态修复技术介绍建议大家多看几遍,一遍远远不够的。

这个链接你一定要点开
这个链接你一定要点开
这个链接你一定要点开
重要的事情说三遍!!!!!!

记得看下, 他是热修复的始祖级的文章,也是本文重点抄袭对象。

我们知道Android系统也是仿照java搞了一个虚拟机,不过它不叫JVM,它叫Dalvik/ART VM他们还是有很大区别的(这是不是我们的重点, 点开是个拓展阅读)。我们只需要知道,Dalvik/ART VM 虚拟机加载类和资源也是要用到ClassLoader,不过Jvm通过ClassLoader加载的class字节码,而Dalvik/ART VM通过ClassLoader加载则是dex。

ClassLoader

在Android中,我们常用的ClassLoader关系如上图,其中BaseDexClassLoader

其中DexClass可以加载apk,jar,及dex文件,但PathClassLoader只能加载已安装到系统中(即/data/app目录下)的apk文件。

Dex加载方式

就让我们来稍微寻找下热修复的突破口。

先来瞅瞅BaseDexClassLoader的源码:

**
 * Base class for common functionality between various dex-based
 * {@link ClassLoader} implementations.
 */
public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;
   
   ……
  }
  
  @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        List suppressedExceptions = new ArrayList();
        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;
    }

5.0的DexPathList部分代码

final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";  
      
     private final Element[] dexElements;
     
    /** List of native library directories. */
    private final File[] nativeLibraryDirectories;
    /**
     * Constructs an instance.
     *
     * @param definingContext the context in which any as-yet unresolved
     * classes should be defined
     * @param dexPath list of dex/resource path elements, separated by
     * {@code File.pathSeparator}
     * @param libraryPath list of native library directory path elements,
     * separated by {@code File.pathSeparator}
     * @param optimizedDirectory directory where optimized {@code .dex} files
     * should be found and written to, or {@code null} to use the default
     * system directory for same
     */
    public DexPathList(ClassLoader definingContext, String dexPath,
            String libraryPath, File optimizedDirectory) {
        if (definingContext == null) {
            throw new NullPointerException("definingContext == null");
        }
        if (dexPath == null) {
            throw new NullPointerException("dexPath == null");
        }
        if (optimizedDirectory != null) {
            if (!optimizedDirectory.exists())  {
                throw new IllegalArgumentException(
                        "optimizedDirectory doesn't exist: "
                        + optimizedDirectory);
            }
            if (!(optimizedDirectory.canRead()
                            && optimizedDirectory.canWrite())) {
                throw new IllegalArgumentException(
                        "optimizedDirectory not readable/writable: "
                        + optimizedDirectory);
            }
        }
        this.definingContext = definingContext;
        ArrayList suppressedExceptions = new ArrayList();
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions);
        if (suppressedExceptions.size() > 0) {
            this.dexElementsSuppressedExceptions =
                suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
        } else {
            dexElementsSuppressedExceptions = null;
        }
        this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
    }
    
     public Class findClass(String name, List 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;
    }

DexFile部分源码

public final class DexFile {
    private long mCookie;
    private final String mFileName;
    private final CloseGuard guard = CloseGuard.get();
    
    ……
    public Class loadClass(String name, ClassLoader loader) {
        String slashName = name.replace('.', '/');
        return loadClassBinaryName(slashName, loader, null);
    }
    public Class loadClassBinaryName(String name, ClassLoader loader, List suppressed) {
        return defineClass(name, loader, mCookie, suppressed);
    }
    private static Class defineClass(String name, ClassLoader loader, long cookie,
                                     List suppressed) {
        Class result = null;
        try {
            result = defineClassNative(name, loader, cookie);
        } catch (NoClassDefFoundError e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        } catch (ClassNotFoundException e) {
            if (suppressed != null) {
                suppressed.add(e);
            }
        }
        return result;
    }
    
    ……
    private static native Class defineClassNative(String name, ClassLoader loader, long cookie)
            throws ClassNotFoundException, NoClassDefFoundError;
}

其中5.0的DexPathList源码中的dexElements(Line 4)就是我们Element(保存有dex信息)的数组。当需要寻找一个class时,BaseDexClassLoader会先调用BaseDexClassLoader中的pathList的findClass方法,而pathList实际上是一个DexPathList对象,查看DexPathList的源码发现,findClass方式其实是去遍历dexElements中的element元素,通过DexFile的对象去loadClass。

热修复就是利用dexElements的顺序来做文章,当一个补丁的patch.dex放到了dexElements的第一位,那么当加载一个bug类时,发现在patch.dex中,则直接加载这个类,原来的bug类可能就被覆盖了。

发版后发现class1.dex中的Bug.class有一个bug,修复后有一个修复好的patch.dex.

CLASS_ISPREVERIFIED问题

根据QQ空间谈到的在虚拟机启动的时候,在verify选项被打开的时候,如果static方法、private方法、构造函数等,其中的直接引用(第一层关系)到的类都在同一个dex文件中,那么该类就会被打上CLASS_ISPREVERIFIED标志,且一旦类被打上CLASS_ISPREVERIFIED标志其他dex就不能再去替换这个类。所以一定要想办法去阻止类被打上CLASS_ISPREVERIFIED标志。

为了阻止类被打上CLASS_ISPREVERIFIED标志,QQ空间开发团队提出了一个方法是先将一个预备好的hack.dex加入到dexElements的第一项,让后面的dex的所有类都引用hack.dex其中的一个类,这样原来的class1.dex、class2.dex、class3.dex中的所有类都引用了hack.dex的类,所以其中的都不会打上CLASS_ISPREVERIFIED标志。

我们可以参考dodola/HotFix项目来说明。

app中有一个LoadBugClass类,他引用了BugClass类。

public class LoadBugClass {
    public String getBugString() {
        BugClass bugClass = new BugClass();
        return bugClass.bug();
    }
}

其中BugClass出现了bug。按照刚才的理论LoadBugClass会被打成CLASS_ISPREVERIFIED标志。为了阻止它打上CLASS_ISPREVERIFIED标志,需要在他的构造函数里添加hack.dex一个类引用,如下所示:

LoadBugClass(){
      …………
      
      System.out.println(dodola.hackdex.AntilazyLoad.class)
  }

其中AntilazyLoad就是hack.dex的类。这样处理不会增加方法数,对代码的侵入较少。

好了这节介绍了Android热修复的实现原理,下一篇会结合jasonross/Nuwadodola/HotFix来谈一下他们的实践。

参考文章


  1. QQ空间 – 安卓App热补丁动态修复技术介绍
  2. Android dex分包方案
  3. 张涛 – Android 热修复,没你想的那么难
  4. 鸿洋 –Android 热补丁动态修复框架小结
  5. Android HotFix方案
  6. classLoader 替换方案 Android 插件化原理解析——插件加载机制