Android热修复原理简述

版权声明:吴延宝原创 https://blog.csdn.net/codeyanbao/article/details/83245419

本文为《2018夯实基础》系列之热修复原理简述
作者:Bob

一、背景

① 为什么会出现热修复技术?

大家都是开发,所以应该都知道有一个东西我们永远也避免不了。不错,**Bug!**我们在开发阶段碰到bug那还好,直接解决就是了,大不了让测试多测一轮。可是,如果我们发出去的版本出现线上Bug,那可怎么办?大多数小的公司可能会选择,重新发新的版本去覆盖安装。这种方式成本较高,而且用户一般都比较讨厌经常需要更新版本的APP。对于小公司来说,出现线上问题影响范围还是比较小的,毕竟用户量较少。可是对于一些巨头公司,有些问题不能及时修复那是致命的。所以呢,有这么一些技术储备和能力比较高的公司为我们开了先河。

② 致敬先辈!

对于像手机QQ空间支付宝360这样的巨头公司当然无法忍受出现线上Bug要等发版解决。所以他们搞出了热修复的技术方案。Android的热修复技术大多都源于此。

③ 原理简述

说了那么多,那到底热修复是怎样的一个技术呢?
在说热修复之前,我们得先知道我们手机中运行的APP实际是我们写的Java代码编译成class文件,然后打包成dex文件运行在手机中的。也就是说,我们手机里面实际运行的是dex文件。所以,如果我们的APP出现线上问题,90%的可能性是我们写的代码导致的Bug,也就是运行在手机里面的dex文件出现了Bug。所以热修复的思想就是静默的下载服务端的新的dex文件替换出现问题的dex文件

二、热修复的实现原理

替换dex文件说起来比较简单,但是真实现起来,还是需要我们考虑更多的细节。下面我们围绕这个替换dex文件详细的分析去实现的步骤:

① Dex分包

我们知道在最开始的时候(ART还没有推出),安卓是使用Dalvik虚拟机来运行我们的应用程序的,安卓项目在打包APK的时候,会将所有编译生成的class文件打包成dex文件。并且Dalvik虚拟机在我们安装应用的时候通过DexOpt工具对dex文件进行优化,DexOpt有个缺点,就是在执行的时候会将dex中的类中的所有方法ID检索出来存在一个链表中,而链表的长度定义的类型为short类型,这就导致dex文件中的方法总数不能超过short的范围,也就是不能超过65536个。显然当我们的项目比较大的时候,这个方法数是不够的。所以后面Android就推出了ART这种本身就支持多dex的APK。

讲了这么多,dex分包和我们这里讲的热修复又有什么关系呢?前面我们已经讲了替换dex的思路,如果只有一个dex,不去拆分,显然我们是没办法替换的。要想替换掉发生Bug的dex,我们首先得有能正常运行的dex代码去做替换这件事吧?所以,我们会拆出一个比较稳定的主dex,作为去实现从服务端下载和替换动作的代码。当其他模块出现Bug时,在去更新对应模块的dex文件。那么一般我们如何去拆分dex呢?

在Android5.0之前呢,我们需要引入谷歌提供的multidex.jar支持multidex。所以需要我们首先在项目的build.gradle中加入如下配置

android {
    defaultConfig {
        multiDexEnabled true
    }
}

dependencies {
    compile ‘com.android.support:multidex:1.0.0}

然后我们借助一个好用的配置分包的第三方库:DexKnife来配置如何分包。这里就不对DexKnife详细介绍了,读者可以去Dexknife的使用文档上查看详细的使用方法。

② 替换dex文件

替换dex文件的逻辑一般是我们进入主页面以后,请求一个接口查询服务端当前是否有新的dex需要替换,如果有就在后台默默下载后去替换。当然这里面包含一些版本、dex名称等参数去区分。如何去下载文件不在本文的介绍范围,相信读者都做过了。这里主要讲解当我们在服务端下载好了一个需要替换的dex包以后,如何将它替换进去。

在讲替换之前,我们首先得了解一下我们的打包的dex文件是如何被加载到虚拟机运行的。Android里面是使用的BaseDexClassLoader去加载dex中的类,所以接下来我们分析下BaseDexClassLoader的源码。

private final DexPathList pathList;

在这个类中,我们首先看到有一个包装类DexPathList是用来存储需要去加载的dex文件列表,我们继续观察DexPathList的源码,发现:

    /**
     * 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数组存储dex文件的路径,看注释可以知道当时的谷歌的工程师蛮有意思,将Facebook会使用反射调用都写进去了,哈哈,调皮。然后我看看他是如何去给将dex文件目录放到dexElements数组中的呢?

    public DexPathList(ClassLoader definingContext, ByteBuffer[] dexFiles) {
    	...省略
        // TODO 这里调用makePathElements()方法
        this.nativeLibraryPathElements = makePathElements(this.systemNativeLibraryDirectories);
		...省略
       
    }
    
    // 我们继续看makePathElements干了什么?
    @SuppressWarnings("unused")
    private static Element[] makePathElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions) {
        // 这里直接调用了makeDexElements方法
        return makeDexElements(files, optimizedDirectory, suppressedExceptions, null);
    }
    
    // 继续追踪makeDexElements方法
     private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
            List<IOException> suppressedExceptions, ClassLoader loader) {
      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) {
          if (file.isDirectory()) {
              // We support directories for looking up resources. Looking up resources in
              // directories is useful for running libcore tests.
              elements[elementsPos++] = new Element(file);
          } else if (file.isFile()) {
              String name = file.getName();

              if (name.endsWith(DEX_SUFFIX)) {
                  // Raw dex file (not inside a zip/jar).
                  try {
                      DexFile dex = loadDexFile(file, optimizedDirectory, loader, elements);
                      if (dex != null) {
                          elements[elementsPos++] = new Element(dex, null);
                      }
                  } catch (IOException suppressed) {
                      System.logE("Unable to load dex file: " + file, suppressed);
                      suppressedExceptions.add(suppressed);
                  }
              } else {
                  DexFile dex = null;
                  try {
                      dex = loadDexFile(file, optimizedDirectory, loader, elements);
                  } catch (IOException suppressed) {
                      /*
                       * IOException might get thrown "legitimately" by the DexFile constructor if
                       * the zip file turns out to be resource-only (that is, no classes.dex file
                       * in it).
                       * Let dex == null and hang on to the exception to add to the tea-leaves for
                       * when findClass returns null.
                       */
                      suppressedExceptions.add(suppressed);
                  }

                  if (dex == null) {
                      elements[elementsPos++] = new Element(file);
                  } else {
                      elements[elementsPos++] = new Element(dex, file);
                  }
              }
          } else {
              System.logW("ClassLoader referenced unknown path: " + file);
          }
      }
      if (elementsPos != elements.length) {
          elements = Arrays.copyOf(elements, elementsPos);
      }
      return elements;
    }

我们可以看到,源码中是将dex文件封装成Element对象存到数组中的。所以,像Facebook或者阿里巴巴、腾讯这样的巨头公司呢看到了其中的本质,也就是Android在加载类的时候,就是在dexElements数组中去遍历dex文件,如果在某一个dex文件中找到了该类就加载。所以,我们的思路是将我们新的修复过Bug的dex文件如果能放到dexElements中的最前面,那么当系统去加载我们出错的类的时候,会优先加载到我们修复过的类了,从而起到修复Bug的作用。

那么,一般是怎么做的呢?首先,我们实例一个BaseDexClassLoader类去加载我们从服务端下载下来的dex文件到内存中,当然这一切需要用到反射去拿到DexPathList类中的dexElements数组,然后将我们的dex文件加载进去成为一个Element对象;然后,我们通过反射拿到我们APP本身的dexElements数组去将我们新的Element放入到最前面。这样就能够让新的dex比有Bug的Dex优先被加载了。

三、小结

本节主要是对使用热修复的来龙去脉,以及我们通过源码的分析找到为什么能够去实现热修复的原因。我们也给大家介绍了使用热修复需要知道的哪些技术点以及业内比较成功的公司。下一节我们将围绕本节的原理以及分析以demo的形式简单实现一个热修复的框架,让读者自己能够轻松实现。

欢迎大家关注转发,共同提高哦~

微信公众号:南京Android部落

猜你喜欢

转载自blog.csdn.net/codeyanbao/article/details/83245419