浅谈Android热修复的前因后果与实现原理。(下)

简介: 最近在集成热修复,正好要进行技术分享,所以就来好好梳理一下 热修复的前因后果。

DexPathList

final class DexPathList {
  //文件后缀
  private static final String DEX_SUFFIX = ".dex";
  private static final String zipSeparator = "!/";
** class definition context */
private final ClassLoader definingContext;
//内部类 Element
private Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
        String librarySearchPath, File optimizedDirectory) {
    this(definingContext, dexPath, librarySearchPath, optimizedDirectory, false);
}
DexPathList(ClassLoader definingContext, String dexPath,
        String librarySearchPath, File optimizedDirectory, boolean isTrusted) {
    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<IOException> suppressedExceptions = new ArrayList<IOException>();
    // save dexPath for BaseDexClassLoader
    //我们关注这个 makeDexElements 方法
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions, definingContext, isTrusted);
    this.nativeLibraryDirectories = splitPaths(librarySearchPath, false);
    this.systemNativeLibraryDirectories =
            splitPaths(System.getProperty("java.library.path"), true);
    List<File> allNativeLibraryDirectories = new ArrayList<>(nativeLibraryDirectories);
    allNativeLibraryDirectories.addAll(systemNativeLibraryDirectories);
    this.nativeLibraryPathElements = makePathElements(allNativeLibraryDirectories);
    if (suppressedExceptions.size() > 0) {
        this.dexElementsSuppressedExceptions =
            suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
    } else {
        dexElementsSuppressedExceptions = null;
    }
}
static class Element {
  //dex文件为null时表示 jar/dex.jar文件
 private final File path;
 //android虚拟机文件在Android中的一个具体实现
 private final DexFile dexFile;
 private ClassPathURLStreamHandler urlHandler;
 private boolean initialized;
 /**
  * Element encapsulates a dex file. This may be a plain dex file (in which case dexZipPath
  * should be null), or a jar (in which case dexZipPath should denote the zip file).
  */
 public Element(DexFile dexFile, File dexZipPath) {
     this.dexFile = dexFile;
     this.path = dexZipPath;
 }
 public Element(DexFile dexFile) {
     this.dexFile = dexFile;
     this.path = null;
 }
 public Element(File path) {
   this.path = path;
   this.dexFile = null;
 }
 public Class<?> findClass(String name, ClassLoader definingContext,
               List<Throwable> suppressed) {
            //核心点,DexFile
           return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                   : null;
       }
 /**
  * Constructor for a bit of backwards compatibility. Some apps use reflection into
  * internal APIs. Warn, and emulate old behavior if we can. See b/33399341.
  *
  * @deprecated The Element class has been split. Use new Element constructors for
  *             classes and resources, and NativeLibraryElement for the library
  *             search path.
  */
 @Deprecated
 public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
     System.err.println("Warning: Using deprecated Element constructor. Do not use internal"
             + " APIs, this constructor will be removed in the future.");
     if (dir != null && (zip != null || dexFile != null)) {
         throw new IllegalArgumentException("Using dir and zip|dexFile no longer"
                 + " supported.");
     }
     if (isDirectory && (zip != null || dexFile != null)) {
         throw new IllegalArgumentException("Unsupported argument combination.");
     }
     if (dir != null) {
         this.path = dir;
         this.dexFile = null;
     } else {
         this.path = zip;
         this.dexFile = dexFile;
     }
 }
  ...
}
 ...
//主要作用就是将 我们指定路径中所有文件转化为DexFile,同时存到Eelement数组中
//为什么要这样做?目的就是为了让findClass去实现
private static Element[] makeDexElements(List<File> files, File optimizedDirectory,
  List<IOException> suppressedExceptions, ClassLoader loader, boolean isTrusted) {
  Element[] elements = new Element[files.size()];
  int elementsPos = 0;
  //遍历所有文件
  for (File file : files) {
      if (file.isDirectory()) {
          //如果存在文件夹,查找文件夹内部查询
          elements[elementsPos++] = new Element(file);
        //如果是文件
      } else if (file.isFile()) {
          String name = file.getName();
          DexFile dex = null;
        //判断是否是dex文件
          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 {
              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);
              }
          }
          if (dex != null && isTrusted) {
            dex.setTrusted();
          }
      } else {
          System.logW("ClassLoader referenced unknown path: " + file);
      }
  }
  if (elementsPos != elements.length) {
      elements = Arrays.copyOf(elements, elementsPos);
  }
  return elements;
}
  ---
 private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader,
Element[] elements)throws IOException {
     //判断可复制文件夹是否为null
     if (optimizedDirectory == null) {
         return new DexFile(file, loader, elements);
     } else {
        //如果不为null,则进行解压后再创建
         String optimizedPath = optimizedPathFor(file, optimizedDirectory);
         return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements);
     }
 }
  -----
public Class<?> findClass(String name, List<Throwable> suppressed) {
    //遍历初始化好的DexFile数组,并由Element调用 findClass方法去生成
    for (Element element : dexElements) {
        //
        Class<?> clazz = element.findClass(name, definingContext, suppressed);
        if (clazz != null) {
            return clazz;
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

上面的代码有点复杂,我摘取了其中一部分我们需要关注的点,便于我们进行分析:


在BaseDexClassLoader中,我们发现最终加载类的是由 DexPathList 来进行的,所以我们进入了 DexPathList 这个类中,我们可以发现 在初始化的时候,有一个关键方法需要我们注意 makeDexElements。而这个方法的主要作用就是将 我们指定路径中所有文件转化为 DexFile ,同时存到 Eelement 数组中。

而最开始调用的 DexPathList中的findClass() 反而是由Element 调用的 findClass方法,而Emement的findClass方法中实际上又是 DexFile 调用的 loadClassBinaryName 方法,所以带着这个疑问,我们进入 DexFile这个类一查究竟。

DexFile

public final class DexFile {
*
 If close is called, mCookie becomes null but the internal cookie is preserved if the close
 failed so that we can free resources in the finalizer.
/
@ReachabilitySensitive
private Object mCookie;
private Object mInternalCookie;
private final String mFileName;
...
DxFile(String fileName, ClassLoader loader, DexPathList.Element[] elements) throws IOException {
     mCookie = openDexFile(fileName, null, 0, loader, elements);
     mInternalCookie = mCookie;
     mFileName = fileName;
     //System.out.println("DEX FILE cookie is " + mCookie + " fileName=" + fileName);
 }
 //关注点在这里
public Class loadClassBinaryName(String name, ClassLoader loader, List<Throwable> suppressed) {
     return defineClass(name, loader, mCookie, this, suppressed);
 }
//
 private static Class defineClass(String name, ClassLoader loader, Object cookie,
                                  DexFile dexFile, List<Throwable> suppressed) {
     Class result = null;
     try {
       //这里调用了一个 JNI层方法
         result = defineClassNative(name, loader, cookie, dexFile);
     } 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, Object cookie,
                                               DexFile dexFile)
         throws ClassNotFoundException, NoClassDefFoundError;

我们从 loadClassBinaryName 方法中发现,调用了 defineClass 方法,最终又调用了 defineClassNative 方法,而 defineClassNative 方法是一个JNI层的方法,所以我们无法得知具体如何。但是我们思考一下,从开始的 BaseDexClassLoader一直到现在的 DexFile,我们一直从入口找到了最底下,不难猜测,这个 defineClassNative 方法内部就是 C/C++帮助我们以字节码或者别的生成我们需要的 dex文件,这也是最难的地方所在。

最后我们再用一张图来总结一下Android 中类加载的过程。


网络异常,图片无法展示
|


在了解完上面的知识之后,我们来总结一下,Android中热修复的原理?

Android中既然已经有了DexClassLoader和 PathClassLoader,那么我在加载过程中直接替换我自己的Dex文件不就可以了,也就是先加载我自己的Dex文件不就行了,这样不就实现了热修复。

真的这么简单吗?热修复的难点是什么?

  • 资源修复
  • 代码修复
  • so库修复

抱着这个问题,如何选用一个最合适的框架,是我们Android开发者必须要考虑的,下面我们就分析一下各方案的差别。

如何选择热修复框架?

目前市场上的热修复框架很多,从阿里热修复网站找了一个图来对比一下:

image.png

简单划分就是3大巨头,阿里,腾讯,美团。并不是谁支持的功能多就用谁,在接入方面我们需要综合考虑。

以我个人的体验来说吧:目前体验了Tinker和 Sophix

Tinker

Tinker的集成有点麻烦,我个人觉得挺简单,而且补丁管理系统 TinkerPatch是收费的(有免费额度),补丁下发慢,大概需要5分钟的等待时间。

Tinker有一个免费版后台,Bugly,补丁管理是免费的,热修复用的Tinker,集成很那啥。。。em,建议多读官网教程看视频,因为有补丁上传监测,下发一个补丁需要5-10分钟等待生效,撤回补丁需要10分钟左右生效,而且一次可能不会生效,后台观察日志需要多次才可以实现补丁撤回。(测试设备:小米5s Plus,Android 8.0)

最后总结:

优点:免费,简单

缺点:集成麻烦,出现问题无法第一时间得到解决方案,毕竟免费的理解一下

性能方法:需要冷启动之后才会生效

Sophix

官网教程详细,完全傻瓜式,响应快,出现问题,解决效率高,毕竟花了钱的。

性能方面:冷启动+即时响应(有条件),

有点:功能最多,支持版本最多,解决问题快

缺点:付费

热修复的注意事项

有了热修复,我们就可以为所欲为了吗?


并不是,热修复受限于各种机型设备,而且也有失败的可能性,所以我们开发者,对于补丁包同样也要抱有敬畏之心。

对于热修复同样也由于严格的过程,但是我们日常开发至少要保证以下几点:

debug-> 打补丁包->开发设备测试->灰度下发(条件下发)->全量下发

下面针对我开发中遇到的问题,给出解决方案。

热修复与多渠道

多渠道打包使用 美团 的一键打包方案。补丁包的话,其实并不会影响,因为补丁包一般改动的代码相同,但前提是需要保证我们每个渠道基准包没问题。如果改动代码有区别,那就需要针对这个渠道单独打补了。

自动化构建与热修复

Android开发一般集成了 Jenkins 或者别的自动化打包工具,我们一般基准包都在 app/build/bakApk 目录下,所以我们可以通过编写 shell 命令,在jenkins中打包时,将生成的基准包移动到一个特定的文件夹即可。tinker,Sophix都是支持服务器后台的,所以我们也可以通过自动化构建工具上传补丁包,如果相应的热修复框架不支持服务器管理的话,那么可以将补丁包上传的指定的文件夹,然后我们app打开时,访问我们的服务器接口下拉最新的补丁包,然后在service中合成。不过 Tinker(bugly) , Sophix 都是支持后台管理,所以具体使用那种方案我们自行选择。


关于热修复的到这里就基本写完了,散散落落居然写了这么多,其实难的不是热修复,而是Android中类加载的过程及一些基础相关知识,理解了这些,我们才能真正明白那些优秀的框架到底是怎样去修复的。


如果本文有帮到你的地方,不胜荣幸。如果有什么地方有错误或者疑问,也欢迎大家提出。

目录
相关文章
|
1月前
|
移动开发 监控 安全
mPaaS常见问题之Android集成dexPatch热修复运行时候无法正常进行热更新如何解决
mPaaS(移动平台即服务,Mobile Platform as a Service)是阿里巴巴集团提供的一套移动开发解决方案,它包含了一系列移动开发、测试、监控和运营的工具和服务。以下是mPaaS常见问题的汇总,旨在帮助开发者和企业用户解决在使用mPaaS产品过程中遇到的各种挑战
34 0
|
10月前
|
安全 Java Shell
Android的热修复技术--阿里的hotfix试用
Android的热修复技术--阿里的hotfix试用
|
dexposed Java AndFix
毕业5年了还不知道Android热修复?
随着移动端业务复杂程度的增加,传统的版本更新流程显然无法满足业务和开发者的需求, 热修复技术的推出在很大程度上改善了这一局面。国内大部分成熟的主流 App都拥有自己的热更新技术,像手淘、支付宝、微信、QQ、饿了么、美团等。
|
监控 Java API
Android IO 框架 Okio 的实现原理,如何检测超时?
在上一篇文章里,我们聊到了 Square 开源的 I/O 框架 Okio 的三个优势:精简且全面的 API、基于共享的缓冲区设计以及超时机制。前两个优势已经分析过了,今天我们来分析 Okio 的超时检测机制。
133 0
|
缓存 Java API
Android IO 框架 Okio 的实现原理,到底哪里 OK?
今天,我们来讨论一个 Square 开源的 I/O 框架 Okio,我们最开始接触到 Okio 框架还是源于 Square 家的 OkHttp 网络框架。那么,OkHttp 为什么要使用 Okio,它相比于 Java 原生 IO 有什么区别和优势?今天我们就围绕这些问题展开。
140 0
|
Java AndFix 开发工具
9012年都过去了,你确定还不学安卓的热修复?(手写AndFix)
在native层进行方法的替换,将错误的方法替换为正确的方法
181 0
9012年都过去了,你确定还不学安卓的热修复?(手写AndFix)
|
缓存 Android开发
深入分析 Android 系统返回手势的实现原理(2)
深入分析 Android 系统返回手势的实现原理(2)
深入分析 Android 系统返回手势的实现原理(2)
|
Java Android开发 开发者
深入分析 Android 系统返回手势的实现原理(1)
深入分析 Android 系统返回手势的实现原理(1)
深入分析 Android 系统返回手势的实现原理(1)
|
缓存 算法 Java
Android 内存缓存框架 LruCache 的实现原理,手写试试?
在之前的文章里,我们聊到了 LRU 缓存淘汰算法,并且分析 Java 标准库中支持 LUR 算法的数据结构 LinkedHashMap。当时,我们使用 LinkedHashMap 实现了简单的 LRU Demo。今天,我们来分析一个 LRU 的应用案例 —— Android 标准库的 LruCache 内存缓存。
140 0
|
存储 移动开发 缓存
浅谈Android热修复的前因后果与实现原理。(上)
最近在集成热修复,正好要进行技术分享,所以就来好好梳理一下 热修复的前因后果。
133 0