热修复 笔记 第一部分 分析篇

Posted xzj_2013

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了热修复 笔记 第一部分 分析篇相关的知识,希望对你有一定的参考价值。

在腾讯直播学习该课程后,记录下学习笔记:

热修复简述

1、什么是热修复

用一个简单的词汇来表述,就是补丁,为了修复某个问题/bug而单独出的一个更新包;
采用百度百科的说法:
热修复补丁(hotfix),又称为patch,指能够修复软件漏洞的一些代码,是一种快速、低成本修复产品软件版本缺陷的方式。
热修复(也称热补丁、热修复补丁,英语:hotfix)是一种包含信息的独立的累积更新包,通常表现为一个或多个文件。这被用来解决软件产品的问题(例如一个程序错误)。通常情况下,热修复是为解决特定用户的具体问题而制作。

2、为什么需要热修复

一个已经上线的app中如果出现了bug,即使是一个非常小的bug,不及时更新的话有可能存在风险,若要及时更新就得将app重新打包发布到应用市场后,让用户再一次下载,这样就大大降低了用户体验。
而当热修复出现之后,我们可以通过加载载补丁包的方式,在用户无感知的情况下修复问题;

3、热修复方案

热补丁方案有很多,其中比较出名的有腾讯Tinker、阿里的AndFix、美团的Robust以及QZone的超级补丁方案。

下面我们就简单了解下各种方案的实现,生效时机,以及涉及到的一些核心技术:

1. 阿里的AndFix修复方案(Native层替换方法)

该方案是通过在native动态替换java层的方法,通过native层hook java层的代码,实现修复。
原理图如下:

实现的方式如下面的代码,通过注解标注要替换的方法,然后通过NDK动态替换掉该方法

其生效时机是:即时生效
涉及到的核心技术:注解以及NDK技术

2.美团的Robus修复方案(Instant Run方法)

该方案的实现原理是对每个函数都在编译打包阶段自动的插入了一段代码。类似于代理,将方法执行的代码重定向到其他方法中
其原理流程如下图:

这个方案实现的方式是在编译期间在每个方法内都插入类似下面一段代码:

其生效时机是:即时生效
涉及到的核心技术:注解、插桩(ASM)、动态代理

3、腾讯Tinker/Qzone修复方案(类加载机制实现方案)
这两个方案是通过类加载器ClassLoader加载Dex的原理实现
只是在生成Dex的方式或者说加载的Dex包有所区别

a.Tinker修复方案
Tinker通过计算对比指定的Base Apk中的dex与修改后的Apk中的dex的区别,补丁包中的内容即为两者差分的描述。运行时将Base Apk中的dex与补丁包进行合成,重启后加载全新的合成后的dex文件。
DexDiff就是腾讯为了计算对比指定的Base Apk中的dex与修改后的Apk中的dex的区别而单独实现的一套算法
其修复流程如下图

其生效时机是:重启生效
涉及到的核心技术:反射、类加载机制、DexDiff

b.Qzone修复方案
QQ空间基于的是dex分包方案。把BUG方法修复以后,放到一个单独的dex补丁文件,让程序运行期间加载dex补丁,执行修复后的方法。
如何做到这一点?
android中所有我们运行期间需要的类都是由ClassLoader(类加载器)进行加载。
因此让ClassLoader加载全新的类替换掉出现Bug的类即可完成热修复。
其修复流程如下图

其生效时机是:重启生效
涉及到的核心技术:反射、类加载机制

ClassLoader实现热修复的详细分析

在了解这一类型的热修复之前,必须要了解Android中的类加载机制。

上面是盗取了直播老师的图,很清晰了展示了android classLoader的继承结构。
首先我们来了解下几个关键类及类中一些重要的方法和属性

  1. BootClassLoader: ClassLoader的内部类兼子类,用来加载Android Framework的class文件。
  2. PathClassLoader: 用来加载自己写的程序中的类。 比如我们自己的activity,service,utils等
  3. DexClassLoader: 功能上与PathClassLoader基本相同,只是在构造方法参数上不同, PathClassLoadaer使用默认opt目录,而DexClassLoader在8.0之前可以指定opt目录,但是8.0之后这个参数失效了。
  4. BaseDexClassLoader:提供了findClass()的实现以及DexPathList成员变量。它的两个子类都是使用它的findClass()
  5. classLoader:提供了loadClass()实现,所有子类均使用它的loadClass()(BootClassLoader除外)。

了解了以上几点之后,我们从Android 6.0源码中分析通过ClassLoader热修复的流程以及涉及到的一些知识点:
第一步:loadClass
既然是覆盖掉class文件,那么我们肯定要知道加载Class的原理,首先我们要去跟踪的就是class的加载
那么class是怎么加载的呢?
上面我们已经了解了,所有我们自己写的类都是通过PathClassLoader来加载的,那么我们首先去看看PathClassLoader的源码
该类是在/libcore/dalvik/src/main/java/dalvik/system/PathClassLoader.java目录下,我们可以使用Source inside 去看本地下载好的源码 也可以通过http://androidxref.com在线查看源码

显然 这个类只提供了两个构造方法,其余的实现都是通过父类的方法,那我们接着跟踪BaseDexClassLoader.java,看看这个类是怎么实现加载class的;

看了这个类的所有方法后,我们发现这个类的方法也比较少,其中比较关键的一个方法就是findclass,但是很显然 这个方法是用于查找class文件的,而我们需要的是加载class的方法,因此我们继续往下跟踪BaseDexClassLoader的父类ClassLoad.java

从这个类中,我们很容易就找到了一个名字叫loadClass的方法,从描述和命名上来看,这个方法就是我们一直在追寻的加载class的方法;
那么这个方法中具体做了些什么?
这个方法的第一句实现
Class<?> clazz = findLoadedClass(className);

看描述,我们就知道 这个方法是用来检查这个类是否被加载过,如果加载过就直接返回
如果没有直接加载过就返回Null
至于对BootClassLoader的类型判断是因为BootClassLoader是用于加载framework的源码类的,如果类型时BootClassLoader,很显然不是我们需要的class
那么继续往下分析
就调用了 clazz = parent.loadClass(className, false);
去看parent定义的地方有这样一行描述The parent ClassLoader.
显然parent就是指的父加载器,这行代码的意思就是在父加载器中查找该类是否被加载过
继续往下跟踪,如果父类没有找到,那么就会调用findclass去加载这个类
这整个流程就是java中的双亲委托加载机制

双亲委托加载机制:

  1. 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
    每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。
  2. 当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.
  3. 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

为什么需要这个委托机制呢?
其实质就是为了避免相同的类被加载多次;

那么继续往下跟踪,这时候就回到了BaseDexClassLoader.java的findClass方法也就是我们的第二步;
第二步:findClass

在findClass方法中我们可以看到主要的实现是调用了该类的一个成员变量 DexPathList pathList中的findClass中;
OK
那我们继续往下跟踪DexPathList的findClass方法:

从这个方法,我们可以看到,在每一个节点中寻找要加载的class,如果找到,停止遍历,立即返回。
首先来看dexElements是什么:

/**
     * 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;

这个数组对应的就是我们apk中的多个dex文件。
总结一下这个过程就是,顺序遍历我们apk中的每一个dex文件,查找我们的class有没有在里面,找到立即返回。
下面是流程的时序图:

很显然到了这一步,我们就很清楚,Qzone的实现方式是怎么一回事了,就是在加载class前把修复后的dex文件插入到这个dexElements元素的最前面,那么就可以实现替换class的目标;

总结

通过对各种修复原理的了解和对ClassLoader加载class的源码分析,我们可以整理如下:
基于类加载机制热修复的思路:

  • 反射获取当前程序的PathClassLoader
  • 反射获取DexPathClassLoader的pathList属性
  • 反射获取pathList中的属性dexElements
  • 把自己的补丁包 patch.dex 转化为 Elements[]数组 pathElements
  • 将pathElements 插入到 dexElements最前面,得到新的 newElements
  • 将newElements反射替换原来的dexElements

以上是关于热修复 笔记 第一部分 分析篇的主要内容,如果未能解决你的问题,请参考以下文章

热修复 笔记 第三部分 优化篇

美团热修复Robust-源码篇

热修复-Nuwa学习篇

探索WebAssembly实现iOS热修复/第一篇/WebAssembly快速上手

Android 热修复方案分析

Android热补丁动态修复