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

一、Tinker 热修复

Tinker通过 Dexdiff 算法将原apk和修复后的apk中的dex文件进行对比,生成差分包,运行时将差分包中的dex和原包中的dex进行合并,从而加载差分包中修复好的类。因为是运行时加载的dex文件,所以修复完成后不能即时生效,需要重启app。

二、Qzone热修复
QQ空间的热修复原理和tinker有异曲同工之处,它基于dex分包方案,把bug类修复完成之后,单独生成一个dex文件,运行期间加载dex补丁,运行的是修复后的类。在Android中所有我们运行期间需要的类都是由ClassLoader(类加载器)进行加载,因此让ClassLoader加载全新的类替换掉出现Bug的类即可完成热修复。所以也需要重启才能生效。

三、AndFix热修复

在native动态替换java层的方法,通过native层hook java层的代码。执行方法时,会直接将修复后的方法再native层进行替换,达到修复的效果,这种方式修复后直接会生效,不需要重启。

四、Robust美团热修复方案

方法运行时会在方法内插入一段代码,如果有修复内容,会将执行的代码重定向到其他方法中。

参考了Instans Run的原理。这种方案也是不需要重启的

五、我们基于QQ空间的热修复方案进行研究

1. ART与Dalvik

什么是Dalvik:

​ Dalvik是Google公司自己设计用于Android平台的Java虚拟机。支持已转换为.dex(Dalvik Executable)格式的Java应用程序的运行,.dex格式是专为Dalvik应用设计的一种压缩格式,适合内存和处理器速度有限的系统。

什么是ART:

Android Runtime, Android 4.4 中引入的一个开发者选项,也是 Android 5.0 及更高版本的默认模式。在应用安装的时候Ahead-Of-Time(AOT)预编译字节码到机器语言,这一机制叫Ahead-Of-Time(AOT)预编译。应用程序安装会变慢,但是执行将更有效率,启动更快。

在Dalvik下,应用运行需要解释执行,常用热点代码通过即时编译器(JIT)将字节码转换为机器码,运行效率低。而在ART 环境中,应用在安装时,字节码预编译(AOT)成机器码,安装慢了,但运行效率会提高。
ART占用空间比Dalvik大(字节码变为机器码), “空间换时间"。
预编译也可以明显改善电池续航,因为应用程序每次运行时不用重复编译了,从而减少了 CPU 的使用频率,降低了能耗。
Dexopt与DexAot

这两个操作是Art架构安装时的操作, ART会执行AOT,但针对Dalvik 开发的应用也能在 ART 环境中运作。

dexopt:对dex文件进行验证和优化,优化后的格式为odex(Optimized dex) 文件
dexAot:在安装时对 dex 文件执行dexopt优化之后,再将odex进行 AOT 提前编译操作,编译为OAT可执行文件(机器码)

2. ClassLoader

Java 类加载器

BootClassLoader , 用于加载Android Framework层class文件。

PathClassLoader ,用于Android应用程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classes.dex

DexClassLoader,加载指定的,以及jar、zip、apk 中的classes.dex。

我们可以在activity中打印来进行验证:

 @Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);/** 测试classLoader的一些使用情况*/// 我们外部的类都是用的PathClassLoaderClassLoader classLoader1 = this.getClassLoader();LogUtils.i("loader1 === " + classLoader1);// 父加载器就是BootClassLoader,所以这个类先从framework中去查找,找不到就从我们本地中查找LogUtils.i("loader1 parent === " + classLoader1.getParent());// framework层的类加载都是用的BootClassLoaderClassLoader classLoader2 = Activity.class.getClassLoader();LogUtils.i("loader  === " + classLoader2);}

打印结果:

3. 源码跟踪

在虚拟机中,加载一个类时,使用的时ClassLoader中的loadClass方法进行加载的,看一下源码:

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{// First, check if the class has already been loaded// 一个类加载后会加入到缓存中,以后加载时从缓存中读取就可以了// 如果找不到就从父亲classLoader中查找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}if (c == null) {// If still not found, then invoke findClass in order// to find the class.c = findClass(name);}}return c;}

代码中可以看到,虚拟机加载一个类时,会从父ClassLoader中查找这个类,父ClassLoade找不到会递归到父亲的父亲,如果祖辈都找不到时,才会使用当前的ClassLoader进行查找。这就是传说中的双亲委托机制。为什么这样做呢?

1、避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。

2、安全性考虑,防止核心API库被随意篡改。

显而易见,这样不管是父加载器还是自己,都会走到findClass()方法:

 @Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {List<Throwable> suppressedExceptions = new ArrayList<Throwable>();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;}

ClassLoader中findCliss方法只抛出了一个异常?那肯定是它的子类重写并实现它了~, 在android中,ClassLoader都是继承了BaseDexClassLoader(可以看PathClassLoader和DexClassLoader, 往上有些人说PathClassLoader可以加载内部类,DexClassLoader才可以加载外部存储卡的文件,其实这两者都可以加载, 没有任何区别),以上代码就是在BaseDexClassLoader中实现了类的查找。里面是通过pathList来进行查找的。继续看pathList(DexPathList.java)中的实现:

public Class findClass(String name, List<Throwable> 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;}

可以看到,dexElements是一个数组, 这里遍历了dexElements,DexFile对象可以看作是dex文件, 如果找到了类直接返回了,这里验证了我们上面所说的。QQ空间热修复就是将修复包patch.dex加入到dexElements开始的位置,当虚拟机加载类时,会先从patch.dex中查找,找到了直接返回,找不到还使用原来的,这样就达到了热修复的效果。

dexElements是一个Element类型的数组,源码中这个Element是私有的,如何创建新的Element并加入到dexElements中呢? 先来看看源码中的dexElements是怎么创建的:

// save dexPath for BaseDexClassLoader
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);

在pathList的构造方法中可以看到,通过makePathElements可以创建一个element数组,所以我们通过反射来调用makePathElements方法创建一个新的数组,再获取到原数组,将两个数组合并到dexElements就可以了。

拿SDK23举例:

首先通过classloader找到pathList对象,

再执行pathList中的makePathElements方法创建补丁包的Element数组

反射拿到原来的dexElements数组

将两个数组进行合并,放到一个新的数组中

再反射修改dexElements,将新数组覆盖调原来的数组,完成热修复。

代码如下:

public static void install(ClassLoader classLoader,File patch) {List<File> patchs = new ArrayList<>();patchs.add(patch);// 查找pathList字段Field pathListField = ReflectUtils.findField(classLoader, "pathList");// 1. 获取pathList对象try {Object pathList = pathListField.get(classLoader);if (pathList == null) {throw new RuntimeException("pathList对象为空");}Method method = ReflectUtils.findMethod(pathList, "makePathElements", List.class, File.class, List.class);ArrayList<IOException> suppressedExceptions = new ArrayList<>();// 2. 补丁包的elements数组Object[] patchElements = (Object[]) method.invoke(null, patchs, null, suppressedExceptions);Field dexElementsField = ReflectUtils.findField(pathList, "dexElements");// 3. 原来的dex数组Object[] oldElements = (Object[]) dexElementsField.get(pathList);// 进行合并// 4. 首先利用反射创建一个盛放两个数组的新数组Object[] newElements = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(),oldElements.length + patchElements.length);// 5. 将两个数组放到新数组中,补丁包的要放在前面System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);System.arraycopy(oldElements, 0, newElements, patchElements.length, oldElements.length);// 6. 将原来的dexElement数组用新数组替换掉dexElementsField.set(pathList, newElements);} catch (IllegalAccessException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();LogUtils.i("error == " + e.getTargetException().getMessage());}}

sdk23 , 19 , 14, 4 这些版本创建dexElement数组的方式不一样,或许是方法名不同,或许是参数不同,需要对这几个版本单独做适配,这里只列举了sdk23的反射方法,其他版本原理相同。同时,这一部分内容可参考Tinker热修复方案来进行适配:tinker方案

android热修复方案相关推荐

  1. Android 热修复方案Tinker(五) SO补丁加载

    基于Tinker V1.7.5 Android 热修复方案Tinker(一) Application改造 Android 热修复方案Tinker(二) 补丁加载流程 Android 热修复方案Tink ...

  2. 干货满满,Android热修复方案介绍

    摘要:在云栖社区技术直播中,阿里云客户端工程师李亚洲(毕言)从技术原理层面解析和比较了业界几大热修复方案,揭开了Qxxx方案.Instant Run以及阿里Sophix等热修复方案的神秘面纱,帮助大家 ...

  3. Android 热修复方案分析

    绝大部分的APP项目其实都需要一个动态化方案,来应对线上紧急bug修复发新版本的高成本.之前有利用加壳,分拆两个dex结合DexClassLoader实现了一套全量更新的热更方案.实现原理在Andro ...

  4. android热修复技术tinker,Android热修复方案第一弹——Tinker篇

    背景 一款App的正常开发流程应该是这样的:新版本上线-->用户安装-->发现Bug-->紧急修复-->重新发布新版本-->提示用户安装更新,从表面上看这样的开发流程顺理 ...

  5. Android 热修复方案Tinker(三) Dex补丁加载

    转载来源:http://blog.csdn.net/l2show/article/details/53307523 之前有说到Tinker的修复原理是跟Qzone类似,这里就详细分析一下为什么这样做可 ...

  6. [读书笔记] 深入探索Android热修复技术原理 (手淘技术团队)

    热修复技术介绍 探索之路 最开始,手淘是基于Xposed进行了改进,产生了针对Android Dalvik虚拟机运行时的Java Method Hook技术--Dexposed. 但该方案对于底层Da ...

  7. Android 热修复 Tinker Gradle Plugin解析

    本文已在我的公众号hongyangAndroid原创首发. 转载请标明出处: http://blog.csdn.net/lmj623565791/article/details/72667669 本文 ...

  8. JAndFix: 基于Java实现的Android实时热修复方案

    简述 JAndFix是一种基于Java实现的Android实时热修复方案,它并不需要重新启动就能生效.JAndFix是在AndFix的基础上改进实现,AndFix主要是通过jni实现对method(A ...

  9. Android 热修复 HotFix 混淆apk生成patch包方案

    android 热修复框架目前了解的有qq空间分包方案的HotFix 和Nuwa,还有阿里开源的AndFix和Dexposed. https://github.com/dodola/HotFix ht ...

最新文章

  1. MATLAB从入门到精通系列-非线性曲线拟合函数lsqcurve()详解
  2. 养成这6个编程好习惯,助你减少90%BUG
  3. 助力产教融合,阿里云教育一体机发布
  4. docker 安装ELK
  5. Asp.Net Core多榜逆袭,这是.NET最好的时代!
  6. CentOS7安装单机版RabbitMQ集群
  7. [Axios] FETCH中的基础语法
  8. Oracle数据库常用undo查询思路
  9. docker和k8s发展史--理解oci/cri/cni/docker swarm/containerd/runc/dockershim
  10. linux-postgresql客户端操作实例
  11. 微信小程序自定义tabbar 图标凸出效果
  12. python画密度散点图_实战Pyhton中matplotlib箱线图的绘制(matplotlib双轴图、箱线图、散点图以及相关系数矩阵图)...
  13. 访问k8s集群出现Unable to connect to the server: x509: certificate is valid for xxx, not xxx问题解决【详细步骤】
  14. origin画已知函数曲线_20+实用origin技能,带你用技巧画出完美曲线!
  15. 微信直接下载app的解决方案
  16. Golang的chan阻塞测试
  17. 【小程序】微信小程序自定义导航栏及其封装
  18. 使用pdf.js把PDF文件转图片
  19. Android BLE 蓝牙开发指南(三)外围设备端开发详解
  20. 远程桌面连接接入路由器的电脑(Windows10)

热门文章

  1. 人工智能岗代替----律师
  2. 看陈广老师c#参考视频总结(第十篇 完)
  3. 杂谈:良好的编程思想。
  4. 如何实现表单label 两端对齐
  5. mysql 多租户_数据层的多租户浅谈(SAAS多租户数据库设计)
  6. Linux中ksh的功能描述,学习Linux中ksh的用法
  7. python chromium 自动化_将 WebDriver (Chromium) 用于测试自动化
  8. h2 mysql 对比_轻量级数据库比较:SQLite、H2和MySQLEmbedded
  9. git 拉取远程项目到本地
  10. Java Mybatis Error selecting key or setting result to parameter object