热修复实现原理浅析 您所在的位置:网站首页 instantrun原理 热修复实现原理浅析

热修复实现原理浅析

2023-11-24 07:44| 来源: 网络整理| 查看: 265

一、热修复框架主要有三种核心技术 1、代码修复 2、资源修复 3、动态库连接修复 二、各种框架总结对比表

在这里插入图片描述 在这里插入图片描述

开发中可根据自己app项目情况合理选择,并不是谁的功能多选择谁的。比如我们需要bug修改后即时生效可以选择阿里的AndFix。不需要即时生效,又需要更强大的功能时可以选择腾讯的Tinker。

三、Instant Run

instant run的出现推动了热修复的发展

1、介绍

Instant Run 是安卓Studio2.0 新增的一个功能(As3.5+后变成了setting->hot swap)。这个功能的出现能够显著提升开发人员二次及以后构建、部署apk的时间。

传统的构建部署和InstantRun 构建部署如下图:

在这里插入图片描述

在这里插入图片描述

(1)项目修改->点击AS的 run 图标后,传统方式的特点:重新构建、安装apk、重启apk。这些过程显然是很耗时的。

(2)项目修改->点击AS的 run 图标后,InstantRun 方式特点:

构建、部署 都是基于更改部分。部署有三种方式,无论哪种部署方式都不会重新安装apk。这里效率就明显提高了。 2、InstantRun 三种部署方式 hot swap:效率最高的一种部署方式。代码的增量改变不需要重启app,甚至不需要重启当前的Activity(AS默认是重启Activity的,可以在setting中关闭默认重启)。

hot swap的适用条件比较少,只有一种情况会被Android Studio视为hot swap类型,就是修改一个现有方法中的代码。

warm swap:不需要重启app,但是Activity必须重启。

只有一种情况会被Android Studio视为warm swap类型,就是修改或删除一个现有的资源文件。

cold Swap:需要重启App。

cold swap相对而言就要更慢一些了,Android Studio会自动记录我们项目的每次修改,然后将修改的这部分内容打成一个dex文件发送到手机上,尽管这种swap类型仍然不需要去安装一个全新的APK,但是为了加载这个新的dex文件,整个应用程序必须进行重启才行。另外,cold swap的工作原理是基于multidex机制来实现的,在不引入外部library的情况下,只有5.0及以上的设备才支持multidex,因此,如果你使用了5.0以下的设备,那么cold swap就无法工作了, 这种情况会执行最原始的完整APK安装过程。

cold Swap触发机制很多如:

添加、删除或修改一个注解添加、删除或修改一个字段添加、删除或修改一个方法添加一个类修改一个类的继承结构修改一个类的接口实现修改一个类的static修饰符涉及资源文件id的改动 3、Instant Run 资源修复原理了解

主要做了两件事,还是挺简单的:

(1) 创建新的AssetManager,通过反射调用AssetManager#addAssetPath 方法来加载外部资源。这样新建的AssetManager就有了外部资源。

(2) 将AssetManager 类型的mAssets字段引用全部替换为新的AssetManager对象。

四、热修复分类 代码修复资源修复动态库连接修复 1、 资源修复:几乎所有的热修复框架的资源修复都是参考的Instant Run方案。 2、动态库的修复:重新加载so 3、代码修复

代码修复有三种方案: 1、底层替换方案:阿里系为主。不会再次加载新类,而是直接native层修改原有类。 2、Instant Run方案:基于字节码操作框架AMS底层实现。为每个方法添加一些代码,来记录方法是否有变化,有变化时就生成替换类。 3、类加载方案:腾讯系为主。基于类加载机制与dex分包方案。

五、代码修复#类加载方案原理 1、为啥会有dex分包

两个原因

65535方法限制:安卓DVM方法调用指令invoke-kind索引为16bit最多能引用65535个方法。LinearAlloc限制:安卓的DVM中的LinearAlloc是一个固定缓冲区,当方法数超过了缓冲区的大小时就会报错。如应用安装时的可能会提示INSTALL_FAILED_DEXOPT 2、Dex 分包方案原理

在应用打包时将应用代码打包成多个dex文件,将应用启动时必须用到的类,和这些类的直接引用类放到主dex中,其他代码放到次dex中。应用启动时先加载主dex,等到应用启动后 在动态加载次dex。从而缓解了主dex的65535方法限制、LinearAlloc限制。

3、Android class loader 的知识点回顾

1、ClassLoader是一个抽象类,BaseDexClassLoader 继承自ClassLoader,是抽象类ClassLoader 的具体实现类,PathClassLoader、DexClassLoader都继承自BaseDexClassLoader 。 2、PathClassLoader 、DexClassLoader源码中无任何自己的实现,继承自BaseDexClassLoader,方法都在父类BaseDexClassLoader 中实现。 3、PathClassLoader 只能加载安装在 Android 系统内 APK 文件(/data/app 目录下),其他位置的文件加载时都会报 ClassNotFoundException。因为 PathClassLoader 会读取 /data/dalvik-cache 目录下的经过 Dalvik 优化过的 dex 文件,这个目录的 dex 文件是在安装 apk 包的时候由 Dalvik 生成的,没有安装的时候,自然没有生成这个文件。 4、DexClassLoader 可从任意目录加载 jar、apk、dex。是实现代码热修复核心

安卓ClassLoader 也是采取的双亲委派策略,首先委派父类开始查找,看下抽象相类ClassLoader.java。

(1)ClassLoader.java

protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { // 1、检查传入的类是否已经加载,如果已经加载就返回该类。 Class c = findLoadedClass(name); if (c == null) { try { if (parent != null) { //2、父加载器是否存在,存在就调用父加载器的loadClass方法 c = parent.loadClass(name, false); } else { //3、父加载器不存在就调用findBootstrapClassOrNull方法,这个方法会直接返回null c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { //4、向上委托流程没有检查出类已经被加载。 //5、调用findClass方法来进行查找流程。 c = findClass(name); } } return c; } private Class findBootstrapClassOrNull(String name) { return null; } protected Class findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }

可知findClass 方法中直接抛出了异常,这说明findClass 方法需要子类来实现。

(2) BaseDexClassLoader.java

public class BaseDexClassLoader extends ClassLoader { private final DexPathList pathList; /** * dexPath: 要加载的程序文件,一般是 dex 文件 * optimizedDirectory: dex 文件解压目录 * librarySearchPath: 加载程序文件时需要用到的库路径 * parent: 父加载器 */ public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, librarySearchPath, null); if (reporter != null) { reporter.report(this.pathList.getDexPaths()); } } //... @Override protected Class findClass(String name) throws ClassNotFoundException { List suppressedExceptions = new ArrayList(); 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 的 findClass 是调用 DexPathList 的 findClass 方法,如果为 null 就抛出 ClassNotFoundException。

(3)DexPathList.java

/*package*/ final class DexPathList { private static final String DEX_SUFFIX = ".dex"; private static final String zipSeparator = "!/"; private final ClassLoader definingContext; /** * dex 文件数组 */ private Element[] dexElements; /** * BaseDexClassLoader 中实例化的是这个构造方法 */ public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) { //... this.definingContext = definingContext; //注意dexElements 数组的创建是在这里完成的 this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext); //... } /** * 生成 Element 集合,Element 里保存着 DexFile 和 文件路径 * 遍历该 APP 私有目录所有文件,拿到以 .dex 结尾的文件 */ private static Element[] makeDexElements(List files, File optimizedDirectory, List suppressedExceptions, ClassLoader loader) { 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(); 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) { 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 文件 */ private static DexFile loadDexFile(File file, File optimizedDirectory, ClassLoader loader, Element[] elements) throws IOException { if (optimizedDirectory == null) { return new DexFile(file, loader, elements); } else { String optimizedPath = optimizedPathFor(file, optimizedDirectory); return DexFile.loadDex(file.getPath(), optimizedPath, 0, loader, elements); } } /** * dex 文件路径集合 */ /*package*/ List getDexPaths() { List dexPaths = new ArrayList(); for (Element e : dexElements) { String dexPath = e.getDexPath(); if (dexPath != null) { dexPaths.add(dexPath); } } return dexPaths; } public Class findClass(String name, List suppressed) { for (Element element : dexElements) { // 1、遍历Element[]数组,调用Element的findClass 方法。 //Element是DexPathList 的静态内部类 Class clazz = element.findClass(name, definingContext, suppressed); if (clazz != null) { return clazz; } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; } /*package*/ static class Element { private final File path; //2、Element内部封装了DexFile,它用于加载dex文件 private final DexFile dexFile; private ClassPathURLStreamHandler urlHandler; private boolean initialized; 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 suppressed) { //3、DexFile不为null就调用DexFile的loadClassBinaryName方法 return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed) : null; } } }

总结:

首先通过ClassLoader#findClass去加载,ClassLoader为抽象类,这个功能交给BaseDexClassLoader#findClass去加载。BaseDexClassLoader交付给 DexPathList#findClass去加载。最终的核心功能就在这个 DexPathList类中:

a、首先DexPathList这个类在构造方法中会初始化其字段Element[] dexElements,Element内部封装了DexFile类,它用于加载dex文件。每个Element 对象对应一个dex文件。这样就把dex从磁盘加载到了内存数组中。

b、然后通过调用DexPathList#findClass遍历Element数组,去Element中查找dex文件。具体是通过Element的findClass查找要加载class文件,方法内部通过DexFile#loadClassBinaryName 去内存中加载这些二进制文件。

c、DexFile#loadClassBinaryName 内部最终通过native方法实现。

4、热修复#代码修复核心原理

类加载方案基于dex分包方案,我们将有bug的类A.class 进行修改,然后再将A.class打包成dex补丁包Patch.dex放到Element[] 的首个元素中。 在类加载时首先会找到Patch.dex这个补丁包中的A.class文件,然后加载。后续的有bug的dex文件中的A.class根据双亲委派机制不会被加载了。 这时候 带有 bug 的 class 就算被 “修复” 了。

注意:类加载方案需要重启app。因为类的生命周期是十分长度,一般情况下是无法被卸载的,想要被卸载就要重启app,也正是如此,类加载方案要重启app才能生效。

The end

参考:

【JVM&DVM的ClassLoader】

【安卓进阶解密】

本章目标:

1、知道常见的热修复方案

2、能够说出类加载方案的原理(重点、重点、重点)

3、了解其他方案的实现原理。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有