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

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

首先,我们需要持有以下几个问题:

  1. 什么是热修复?它可以帮我解决什么问题?
  2. 热修复的产生背景?
  3. 热修复的基本原理是什么?
  4. 如何选择热修复框架?
  5. 热修复的注意事项
  6. 热修复与多渠道?
  7. 自动化构建与热修复?


上面一共有7个问题,如果是新同学的话,后面两条可能不会很了解,建议自行补课学习。于是最基本的5个问题,我们必须明白,这是我们每个开发者学习一个新知识的基本需要做到的。


测试环境 :Android 9.0 -P

什么是热修复?它可以帮我解决什么问题?

其实简单来说,热修复就是一种动态加载技术,比如你线上某个产品此时出现了bug:

传统流程:debug->测试->发布新版 ->用户安装(各平台审核时间不一,而且用户需要手动下载或者更新)

集成热修复情况下:dubug->测试->推送补丁->自动下载补丁修复 (用户不知情况,自动下载补丁并修复)


对比下来,我们不难发现,传统流程存在这几大弊端:

  1. 发版代价大
  2. 用户下载安装的成本过高
  3. bug修复不及时,取决于各平台的审核时间等等

热修复产生背景?

  • app发版成本高
  • 用H5集成某些经常变动的业务逻辑,但这种方案需要学习成本,而且对于无法转为H5形式的代码仍旧是无法修复;
  • Instant Run


上面三个原因中,我们主要来谈一下 Instant Run:


Android Studio2.0时,新增了一个 Instant Run的功能,而各大厂的热修复方案,在代码,资源等方面的实现都是很大程度上参考了Instant Run的代码。所以可以说 Instant Run 是推进Android 热修复的主因。

那 Instant Run内部是如何做到这一点呢?

 1.构建一个新的 AssetManager(资源管理框架),并通过反射调用这个 addAssetPath,把这个完整的新资源加入到 AssetManager中,这样就得到了一个含有所有新资源的 AssetManager.

 2.找到所有之前引用到原有AssetManager的地方,通过反射,把引用出替换为新的AssetManager.

热修复的原理是什么?

我们都知道热修复都相当于动态加载,那么动态加载到底动态在哪里了呢。

说到这个就躲不过一个关键点 ClassLoader(类加载器) ,所以我们先从Java开始。



我们都知道Java的类加载器有四种,分别为:

  • Bootstarp ClassLoader
  • Extension ClassLoader
  • App ClassLoader 加载应用ClassLoader
  • Custom ClassLoader 加载自己的class文件


类加载过程如下:

过程:加载-连接(验证-准备-解析)-初始化

 1.加载

   将类的信息(字节码)从文件中获取并载入到JVM的内存中

 2.连接
   验证:检查读入的结构是否符合JVM规范
   准备:分配一个结构来存储类的信息
   解析:将类的常量池中的所有引用改变成直接引用

 3.初始化

    执行静态初始化程序,把静态变量初始化成指定的值

其中用到的三个主要机制:

  1. 双亲委托机制
  2. 全盘负责机制
  3. 缓存机制

其实后面的两个机制都是主要从双亲委托机制延续而来。

在说明了Java 的ClassLoader之后,我们接下来开始Android的ClassLoader,不同于Java的是,Java中的ClassLoader可以加载 jar 文件和 Class文件,而Android中加载的是Dex文件,这就需要重新设计相关的ClassLoader类。所以Android 的ClassLoader 我们会说的详细一点


源码解析

在这里,顺便提一下,这里贴的代码版本是Android 9.0,在8.0以后,PathClassLoader和DexClassLoader并没有什么区别,因为唯一的一个区别参数 optimizedDirectory已经被废弃。

首先是 loadClass,也就是我们类加载的核心方法方法:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // First, check if the class has already been loaded
        //查找当前类是否被加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    //查看父加载器是否加载过
                    c = parent.loadClass(name, false);
                } else {
                    //如果没有加载过,调用根加载器加载,双亲委托模式的实现
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            //找到根加载器依然为null,只能自己加载了
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                c = findClass(name);
            }
        }
        return c;
}

这里有个问题,JVM双亲委托机制可以被打破吗?先保留疑问。

我们主要去看他的 findClass方法

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

这个方法是一个null实现,也就是需要我们开发者自己去做。

从上面基础我们知道,在Android中,是有 PathClassLoader和 DexClassLoader,而它们又都继承与 BaseDexClassLoader,而这个BaseDexClassLoader又继承与 ClassLoader,并将findClass方法交给子类自己实现,所以我们从它的两个子类 PathClassLoader和 DexClassLoader入手,看看它们是怎么处理的。

public class PathClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code PathClassLoader} that operates on a given list of files
     * and directories. This method is equivalent to calling
     * {@link #PathClassLoader(String, String, ClassLoader)} with a
     * {@code null} value for the second argument (see description there).
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param parent the parent class loader
     */
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    /**
     * Creates a {@code PathClassLoader} that operates on two given
     * lists of files and directories. The entries of the first list
     * should be one of the following:
     *
     * <ul>
     * <li>JAR/ZIP/APK files, possibly containing a "classes.dex" file as
     * well as arbitrary resources.
     * <li>Raw ".dex" files (not inside a zip file).
     * </ul>
     *
     * The entries of the second list should be directories containing
     * native library files.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @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 PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

由注释看可以发现PathClassLoader被用来加载本地文件系统上的文件或目录,因为它调用的 BaseDexClassLoader的第二个参数为null,即未传入优化后的Dex文件。

注意:Android 8.0之后,BaseClassLoader第二个参数为(optimizedDirectory)为null,所以DexClassLoader与PathClassLoader并无区别

DexClassLoader

public class DexClassLoader extends BaseDexClassLoader {
    /**
     * Creates a {@code DexClassLoader} that finds interpreted and native
     * code.  Interpreted classes are found in a set of DEX files contained
     * in Jar or APK files.
     *
     * <p>The path lists are separated using the character specified by the
     * {@code path.separator} system property, which defaults to {@code :}.
     *
     * @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 this parameter is deprecated and has no effect since API level 26.
     * @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 DexClassLoader(String dexPath, String optimizedDirectory,
            String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

DexClassLoader用来加载jar、apk,其实还包括zip文件或者直接加载dex文件,它可以被用来执行未安装的代码或者未被应用加载过的代码,也就是我们修复过的代码。

注意:Android 8.0之后,BaseClassLoader第二个参数为(optimizedDirectory)为null,所以DexClassLoader与PathClassLoader并无区别


从上面我们可以看到,它们都继承于BaseDexClassLoader,并且它们真正的实现行为都是调用的父类方法,所以我们来看一下BaseDexClassLoader.

BaseDexClassLoader

public class BaseDexClassLoader extends ClassLoader {
  private static volatile Reporter reporter = null;
  //核心关注点
   private final DexPathList pathList;
   public BaseDexClassLoader(String dexPath, File optimizedDirectory,
           String librarySearchPath, ClassLoader parent) {
        //classloader,dex路径,目录列表,内部文件夹
       this(dexPath, optimizedDirectory, librarySearchPath, parent, false);
   }
   public BaseDexClassLoader(String dexPath, File optimizedDirectory,
           String librarySearchPath, ClassLoader parent, boolean isTrusted) {
       super(parent);
       this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted);
       if (reporter != null) {
           reportClassLoaderChain();
       }
   }
  ...
    public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        this.pathList = new DexPathList(this, dexFiles);
    }
  //核心方法
    @Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
        //异常处理
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        //这里也只是一个中转,关注点在 DexPathList
        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;
    }
  ...
}

从上面我们可以发现,BaseDexClassLoader其实也不是主要处理的类,所以我们继续去查找 DexPathList.

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