热修复原理:

目录

热修复的产生:

热修复框架的种类和对比:

资源修复:

1.Instant Run概述:

2.Instant Run的资源修复:

2.1:总结:

代码修复:

1.类加载方案:

1.1:65536限制:

1.2:LinearAlloc限制:

1.3:Dex分包方案:

1.4:继续---类加载方案:

2.底层替换方案:

3.Instant Run方案:

动态链接库的修复:

1.动态库和静态库:

2.System的load方法和loadLibrary方法:

2.1:System的load方法:

2.2:System的loadLibrary方法:

2.3:nativeLoad方法:

2.3.1:LoadNativeLibrary函数总结:

2.3.2:so修复方案:


在Android中,热修复技术越来越多的在被使用,也出现了很多的热修复框架,比如:AndFix,Tinnker,Dexposed和Nuwa等。

热修复的产生:

在开发过程中我们有可能遇到如下的情况:

  • 刚发布的版本出现了严重的Bug,这就需要去解决Bug、测试并打包在各个应用市场上重新发布,这会耗费大量的人力和物力,代价会比较大。
  • 已经改正了此前发布版本的 Bug,如果下一个版本是一个大版本,那么两个版本的间隔时间会很长,这样要等到下个大版本发布再修复Bug,此前版本的 Bug会长期地影响用户。
  • 版本升级率不高,并且需要很长时间来完成版本覆盖,此前版本的Bug就会一直影响不升级版本的用户。
  • 有一个小而重要的功能,需要短时间内完成版本覆盖。

热修复框架的种类和对比:

热修复的框架种类繁多,但是热修复框架的核心技术主要有3种:分别是代码修复资源修复动态链接库修复。其中每一个核心技术又有很多不同的技术方案,每一个技术方案又有不同的实现,且还在不断的更新迭代。

不同公司的热修复框架
类别成员
阿里系AndFix,Dexposed,阿里百川,Sophix
腾讯系微信的Tinker,QQ空间的超级补丁,QQ的QFix
知名公司美团的Robust,饿了么的Amigo,蘑菇街的Aceso
其他RocooFix,Nuwa,AnoleFix

部分热修复框架的对比
特性AndFixTinker/AmigoQQ空间Robust/Aceso
即时生效
方法替换
类替换
类结构修改
资源替换
so替换
支持gradle
支持ART

支持Android7.0

资源修复:

1.Instant Run概述:

Instant Run是Android Studio2.0以后新增加的一个运行机制,能够显著的减少开发人员第二次以及以后构建和部署时间。在没有使用Instant Run之前,我们编译和部署应用程序的流程为:

我们看到传统的编译部署需要重新安装App和重启App,这种情况显然会很耗时,Instant Run会避免这种情况。

从图中可以看出 Instant Run的构建和部署都是基于更改的部分的。Instant Run部署有三种方式,Instant Run会根据代码的情况来决定采用哪种部署方式,无论哪种方式都不重新安装App,这一点就已经提高了不少的效率。分别是:

  1. Hot swap:从名称也可以看出Hot Swap是效率最高的部署方式,代码的增量改变不需要重启App,甚至不需要重启当前的Activity。修改一个现有方法中的代码时会采用Hot Swap。
  2. Warm Swap: App不需重启,但是Activity需要重启。修改或删除一个现有的资源文件时会采用Warm Swap。
  3. Cold Swap:App需要重启,但是不需要重新安装。采用Cold Swap的情况很多,比如添加、删除或修改一个字段和方法、添加一个类等。

2.Instant Run的资源修复:

我们来了解一下Instant Run的资源修复原理,因为Instant Run部署Android的源码,我们需要反编译获取。Instant Run资源修复的核心逻辑在Monkey Patcher的monkeyPatchExistingResources方法中:首先创建一个新的AssetManager,创建过程为AssetManager.class.getConstructor(new Class[0]).newInstance(new object[0]),然后通过反射调用addAssetPath方法来加载外部SD卡的资源,具体为AssetManager.class.getDeclaredMethod()和mAddAssetPath.invoke()。接着遍历Activity列表获取每个activity的Resource,并且后续通过反射得到Resource的AssetManager类型的mAssets字段,具体为Resource.class.getDeclaredField("mAssets")。然后给这个mAssets字段设置新的AssetManager。最后根据SDK版本的不同,用不一样的方式得到Resource的弱引用集合,再遍历这个集合,将弱引用集合中的Resource的mAssets字段的引用都替换为新创建的AssetManager。

com/android/tools/fd/runtime/MonkeyPatcher.java

AssetManager主要用于存储一些常用的资源,供项目使用。比如存放图片,Json文件,TextView自定义字体等等资源。下面讲解一下每个资源的使用方法。当我们在组件中获取资源时使用getResource获得Resource对象,通过这个对象我们可以访问相关资源,因为在Resource 中有一个AssetManager的全局变量,在Resource的构造函数中传入的,所以最终获取资源都是通过 AssetManager 获取的。

2.1:总结:

Instant Run中的资源修复大概分为两个步骤,即:

  1. 创建新的AssetManager,并且通过反射调用addAssetPath方法加载外部的资源,这样新创建的AssetManager就有了外部资源。
  2. 将AssetManager类型的mAssets字段的引用全部替换为新创建的AssetManager。

代码修复:

代码修复主要有三个方案,分别是底层替代方案类加载方案Instant Run方案

1.类加载方案:

类加载方案基于Dex分包方案,什么是Dex分包方案,要从65536限制和LinearAlloc限制说起。

1.1:65536限制:

随着应用功能越来越复杂,代码量不断地增大,引入的库也越来越多,可能会在编译时出现以下异常:com.android.dex.DexIndexOverflowException: method ID not in [0,0xffff]: 65536。这说明应用中引用的方法数超过了最大数65536个。产生这一问题的原因就是系统的65536限制,65536限制的主要原因是DVM Bytecode的限制,DVM指令集的方法调用指invoke-kind索引为16bits,最多能引用65535个方法。

1.2:LinearAlloc限制:

在安装应用时可能会提示INSTALL_FAILED_DEXOPT,产生的原因就是 LinearAllocDVM中的LinearAlloc是一个固定的缓存区,当方法数超出了缓存区的大小时会报错。

1.3:Dex分包方案:

为了解决65536限制和LinearAlloc限制,从而产生了Dex分包方案。Dex分包方案主要做的是在打包时将应用代码分成多个Dex,将应用启动时必须用到的类和这些类的直接放到主Dex中,其他代码放到次Dex中。当应用启动时先加载主Dex,等到应用启动态地加载次Dex,从而缓解了主Dex的65536限制和LinearAlloc限制。Dex分包方案主要有:Google官方方案、Dex自动拆包和动态加载方案。

1.4:继续---类加载方案:

我们在理解ClassLoader中写到过类加载器的加载过程,其中有一个环节就是调用DexPathList的findClass方法:Element内部封装了DexFile, DexFile用于加装 dex文件,因此每个dex文件都对应着一个Element。多个Element组成了Element数组dexElements。当要查找类时,会遍历Element 数组 dexElements(相当于遍历dex文件数组),后续调用Element的findClass方法,其方法内部会调用DexFile的loadClassBinaryName方法查找类,如果在Element(dex文件)找到了该类就返回,如果没有找到就接着在下一个Element进行查找。根据上面的查找流程,我们将有Bug的类Key.class进行修改,再将Key.class打包成为包含dex的补丁包Patch.jar,放在Element数组 dexElements的第一个元素,这样会首先找到Patch.dex中的Key.class去替换之前存在Bug的Key.class,排在数组后面的dex文件中存在 Bug的Key.class根据ClassLoader的双亲委托模式就不会被加载,这就是类加载方案。

因此实际上,一种热修复实现可以将出现Bug的class单独的制作一份fix.dex文件(补丁包),然后在程序启动时,从服务器下载fix.dex保存到某个路径,再通过fix.dex的文件路径,用其创建Element对象,然后将这个Element对象插入到我们程序的类加载器PathClassLoaderpathList中的dexElements数组头部。这样在加载出现Bug的class时会优先加载fix.dex中的修复类,从而解决Bug。

类加载方案需要重启APP后让ClassLoader重新加载新的类,为什么需要重启呢?是因为类是无法被卸载的,要想重新加载新的类就需要重启App,因此采用类加载方热修复框架是不能即时生效的。虽然很多热修复框架采用了类加载方案,但具体的实现和步骤还是有一些区别的,比如QQ空间的超级补丁和Nuwa是按照上面说的将补丁放在Element数组的第一个元素得到优先加载。微信Tinker 将新旧APK做了diff,得到patch.dex,再将patch.dex与手机中APK的classes.dex做合并,生成新的classes dex,然后在运行时通过反射将classes.dex放在Element数组的第一个元素。饿了么的Amigo则是补丁包中每个dex对应的 Element取出来,之后组成新的Element数组,在运行时通过反射用新的Element数组替换掉现有的Element数组。

2.底层替换方案:

与类加载方案不同的是,底层替换方案不会再次加载新类,而是直接在Native层修改,由于在原有类进行修改限制会比较多,且不能增减原有类的方法和字段,如果增加了方法数,那么方法索引数也会增加,这样访问方法时会无法通过索引找到正确的方法,同样的字段也是类似的情况。底层替换方案和反射的原理有些关联,就拿方法替换来说,方法反射我们可以调用java.lang.Class.getDeclaredMethod,假设我们要反射Key的show方法,会调用如下所示的代码:
Key.class.getDeclaredMethod("show").invoke (Key.class.newInstance())。

getDeclaredMethod方法返回一个 Method 对象,该对象反映此 Class 对象所表示的类或接口的指定声明方法。

Android8.0的invoke方法是一个native方法,它在JNI层的代码为:Method_invoke函数,Method_invoke函数内部调用了InvokeMethod函数,在该方法中获取传入的javaMethod(Key的show方法)在ART虚拟机中对应的一个ArtMethod指针,ArtMethod结构体中包含了Java方法的所有信息,包括了执行入口,访问权限,所属类和代码执行地址等等在ArtMethod结构中比较重要的字段是dex_cache_resolved_methods和entry_point_from_quick_compiled_code,它们是方法的执行入口,当我们调用某一个方法时(比如Key的show方法),就会取得show方法的执行入口,通过执行入口就可以跳过去执行show方法。替换ArtMethod结构体中的字段或者替换整个ArtiMethod结构体,这就是底层替换方案。AndFix采用的是替换ArtMethod结构体中的字段,这样会有兼容问题,因为厂商可能会修改ArtMethod结构体,导致方法替换失败。Sophix采用的是替个 ArtMethod结构体,这样不会存在兼容问题。底层替换方案直接替换了方法,可以立即生效不需要重启。采用底层替换方案主要是阿里系为主,包括AndFix、Dexposed、阿里百川、Sophix等等。

3.Instant Run方案:

除了资源修复,代码修复同样也可以借鉴 Instant Run的原理,可以说Instant Run的出现推动了热修复框架的发展。Instant Run在第一次构建APK时,使用ASM在每一个方法中注入了类似如下的代码:

IncrementalChange localIncrementalChange=$change;
     if(localIncrementalChange!=null){
          localIncrementalChange.access$dispatch(
               "onCreate.(Landroid/os/Bundle;)V",new object[] {this,
                     paramBundle});
     return;
}

首先创建了一个成员变量localIncrementalChange,它的值为$change,$change实现了IncrementalChange这个抽象接口。当我们点击InstantRun 时,如果方法没有变化,则$change为null,就调用return,不做任何处理。如果方法有变化,就生成替换类,这里我们假设MainActivity的onCreate方法做了修改,就会生成替换类MainActivity$override,这个类实现了IncrementalChange接口,同时也会生成一个AppPatchesLoaderlmpl类,这个类的getPatchedClasses 方法会返回被修改的类的列表(里面包含了MainActivity),根据列表会将MainActivity的$change设置为MainActivity$override,接着会执行 MainActivity$override的access$dispatch方法,在access$dispatch方法中会根据参数"onCreate.(Landroid/os/Bundle;)V"执行‘MainActivity$override的onCreate方法,从而实现了onCreate方法的修改。借鉴 Instant Run的原理的热修复框架有Robust和Aceso。


什么是ASM?
ASM是一个Java字节码操控框架,它能够动态生成类或者增强现有类的功能。ASM可以直接产生class文件,也可以在类被加载到虚拟机之前动态改变类的行为。

动态链接库的修复:

Android平台的动态链接库主要指的是so库,热修复框架的so的修复主要是更新so,也就是重新加载so。

1.动态库和静态库:

  • 在Win下,动态库以.dll结尾,静态库以.lib结尾。
  • 在Linux下,动态库文件以.so结尾,静态库以.a结尾。
  • 在Mac下,动态库以.dylib结尾,静态库以.a结尾。
  • 利用静态函数库编译成的文件比较大,因为整个函数库在编译时都会被整合进目标代码中,他的优点就显而易见了,即编译后的执行程序不需要外部的函数库支持,因为所有使用的函数都已经被编译进去了。当然这也会成为他的缺点,因为如果你静态链接的函数库改变了,那么你的程序必须重新编译。

  • 动态函数库在编译的时候并没有被编译进目标代码中,你的程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。 动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。

  • 某个程序在运行时要调用某个动态链接库函数的时候,OS首先查看所有正在运行的进程,找找看是否已经有人载入了这个库。如果有的话,直接用。如果没有才会载入。这样的第一个优点就是节省内存空间。动态调入意味着是等需要的时候才调入内存,而不是不管用不用都要先放到内存里来。

2.System的load方法和loadLibrary方法:

加载so主要用到了System的load方法和loadLibrary方法。System的load方法传入的参数是so在磁盘的完整路径,用于加载指定路径的so。而System的loadLibrary方法传入的参数是so的名称,用于加载APP安装之后自动从apk包中复制到/data/data/packagename/lib下的so。目前so的修复都是基于这两个方法。

2.1:System的load方法:

System的load方法把传入的参数(so在磁盘的完整路径),传递给了内部的Runtime.getRuntime().load0()方法。其中Runtime.getRuntime()用于得到当前Java应用程序的运行环境的Runtime,而load0()方法又调用了doLoad方法,并且将加载该类的类加载器作为参数传递进去。doLoad方法中调用了native方法nativeLoad方法。nativeLoad方法后续会写到。

2.2:System的loadLibrary方法:

System的loadLibrary方法,它会调用Runtime的loadLibrary0方法,在该方法内部首先判断传入的ClassLoader是否为null,如果为null的话,则遍历getLibPaths方法,这个方法会返回java.library.path选项配置的路径数组。并且后续拼接出so路径,传递给doLoad方法。如果不为null的话,调用ClassLoader的findLibrary方法,再把findLibrary方法的返回值传递给doLoad方法中,该方法会调用native方法的nativeLoad方法

findLibrary方法在BaseDexClassLoader类中实现。findLibrary方法调用了DexPathList的findLibrary方法,该方法中有一个NativeLibraryElement数组,数组中的每一个NativeLibraryElement都对应着一个so库,接着调用NativeLibraryElement的findNativeLibrary方法就可以返回so的路径。

2.3:nativeLoad方法:

nativeLoad方法对应的JNI层的函数是Runtime_nativeLoad函数中调用了JVM_NativeLoad函数。该函数获取一个JavaVMExt类型的指针,其中JavaVMExt用于代表一个虚拟机的实例,接着调用了JavaVMExt的LoadNativeLibrary函数来加载so。

LoadNativeLibrary函数主要做了:根据so的名称从libraries_中获取对应的SharedLibrary类型的指针library,如果之前已经加载过该so,或者之前加载使用的ClassLoader和当前传入的ClassLoader不一样,或者上次加载so的结果有异常,就不会重复加载so。接着通过OpenNativeLibrary方法打开该so,并且返回so的句柄,获取so句柄失败的话就会返回false,中断so加载。然后创建SharedLibrary,并且将so句柄传进去。接着通过传入的path获取对应的library,如果它为null,则将创建的SharedLibrary赋值给它,并且将library存储到libraries_中。然后查找JNI_OnLoad函数,如果找到了,则执行JNI_OnLoad函数,并且赋值给version,根据version值的不同,返回不一样的was_successful。

2.3.1:LoadNativeLibrary函数总结:
  1. 判断so是否被加载过,两次的ClassLoader是否是同一个,避免so被重复加载。
  2. 打开so并且得到so的句柄,获取失败则返回false。然后创建新的SharedLibrary,接着判断传入的path对应的library是否为null,是则把新的SharedLibrary赋值给它,并且将library存储到libraries_中。
  3. 查找JNI_OnLoad函数,根据不同的情况设置was_successful并且返回。
2.3.2:so修复方案:
  1. 将so补丁插入到NativeLibraryElement数组的前部,使so补丁的理解先被返回和加载。
  2. 调用System的load方法来接管so的加载入口。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mo@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值