AndFix同样是一种底层替换的方案,两个方案使用

作者:美狮美高梅官方网站

深入探索Android热修复技术原理这本书主要讲解了Android的热修复中的热部署,冷部署以及资源和so库的修复技巧。全文主要讲Sophix应对以上四个方面的技术解析,不管是自家产品还是业界其他方案的横纵对比,Sophix技术目前都是最优的。

读书笔记,写写画画记忆更深刻,如果还能梳理一下的话,那就更好了。

我将热修复原理落地实践MyHotFix

  1. 补丁小,合成不占太多空间和性能。
  2. 对代码的侵入小,对native代码的hook也精简,做到最大兼容。
  3. 支持的修复范围广。支持小范围的即时生效和大范围的冷启动。也支持so库和资源修复。

热修复技术介绍

1.热修复技术介绍

在事件分发流中,通过Hook钩子在事件传送到终点前截获并监控事件的传输,从而处理一些特定干预事件。

探索之路

最开始,手淘是基于Xposed进行了改进,产生了针对Android Dalvik虚拟机运行时的Java Method Hook技术——Dexposed。

但该方案对于底层Dalvik结构过于依赖,最终无法兼容Android 5.0 以后的ART虚拟机,因此作罢。

后来支付宝提出了新的热修复方案AndFix。

AndFix同样是一种底层替换的方案,也达到了运行时生效即时修复的效果,并且重要的是,做到了Dalvik和ART环境的全版本兼容

阿里百川结合手淘在实际工程中使用AndFix的经验,对相关业务逻辑解耦后,推出了阿里百川HotFix方案,并得到了良好的反响。

此时的百川HotFix已经是一个很不错的产品了,对基本的代码修复需求都可以解决,安全性和易用性都做的比较好。然而,它所依赖基石,AndFix本身是有局限性的。且不说其底层固定结构的替换方案不好,其使用范围也存在着诸多限制,虽然可以通过改造代码绕过限制来达到相同的修复目的,但这种方式即不优雅也不方便。而更大的问题,AndFix只提供了代码层面的修复,对于资源和so的修复都未能实现。

在Android平台上,业界除了阿里系之外,比较著名的修复还有:腾讯QQ空间的超级补丁技术、微信的Tinker、饿了么的Amigo、美团的Robust等等。不过他们各自有自身的局限性,或者不够稳定,或者补丁过大,或者效率低下,或者使用起来过去繁琐,大部分技术上看起来似乎可行,但实际体验并不好。

终于在2017年6月,阿里巴巴手淘技术团队联合阿里云正式发布了新一代的非侵入式的Android热修复方案——Sophix。

Sophix的横空出世,打破了各家热修复技术纷争的局面。因为我们可以满怀信心的说,在Android热修复的三大领域:代码修复、资源修复、so修复方面,以及方案的安全性和易用性方面,Sophix都做到了业界领先。

Sophix的诞生,期初是对原先的阿里百川的HotFix 1.X版本进行升级衍进。

Sophix保留了阿里百川HotFix的服务端整套请求流程,以及安全校验部分。

而原本的热修复方案,主要限制在于AndFix本身。

AndFix自身的限制几乎是无法绕过的,在运行时对原有类机构是已经固化在内存中的,它的一些动态属性很难进行扩展。
并且由于Android系统的碎片化,厂商的虚拟机底层结构都不是确定的,因此直接基于原先机制进行扩展的风险很大。

1.1 什么是热修复

为了修复刚发版时出现的紧急bug,无需重新发版!

  • Java API Hook通过对Android平台的虚拟机注入与Java反射的方式,来改变Android虚拟机调用函数的方式(ClassLoader),从而达到Java函数重定向的目的。参考

方案对比

方案对比 Sophix Tinker Amigo
Dex修复 同时支持即时生效和冷启动修复 冷启动修复 冷启动修复
资源更新 差量包,不用合成 差量包,需要合成 全量包,不用合成
SO库更新 插桩实现,开发透明 替换接口,开发不透明 插桩实现,开发透明
性能损耗 低,仅冷启动情况下有些损耗 高,有合成操作 低,全量替换
四大组件 不能增加 不能增加 能增加
生成补丁 直接选择已经编好的新旧包在本地生成 编译新包时设置基线包 上传完整新包到服务端
补丁大小
接入成本 傻瓜式接入 复杂 一般
Android版本 全部支持 全部支持 全部支持
安全机制 加密传输及签名校验 加密传输及签名校验 加密传输及签名校验
服务端支持 支持服务端控制 支持服务端控制 支持服务端控制

可以看到,Sophix在各个指标上都占优势。而其中唯一不支持的地方就是四大组件的修复。

这是因为,如果要修复四大组件,必须在AndroidManifest里面预先插入代理组件,并且尽可能声明所有权限、而这么做就会给原先的app添加很多臃肿的代码,对app运行流程的侵入性很强,所以,本着对开发者透明与代码极简的原则,这里不做多余处理。

1.2 技术积淀

手淘基于Xposed进行改进,产生针对Android Dalvik虚拟机的Java Method Hook技术的Dexposed。
支付宝提出AndFix方案,可以做到在Dalvik和Art全平台兼容的即时修复
阿里百川结合手淘实际使用AndFix的经验,解耦后推出HotFix方案
2017手淘联合阿里云推出Sophix热修复方案
其余著名热修复方案有:
QQ空间超级补丁、微信Tinker
饿了么Amigo
美团Robust
360RePlugin
滴滴出行VirtualAPK
uwa

Sophix同时使用了热启动的底层替换方案及冷启动的类加载方案,两个方案使用的补丁是相同的。优先热启动。

设计理念

Sophix的核心设计理念——就是非侵入性。

在Sophix中,唯一需要的就是初始化和请求补丁两行代码,甚至连入口Application类我们都不做任何修改,这样就给了开发者最大的透明度和自由度。

2.代码热修复技术

新美高梅网站 1sophix与主流框架对比

代码修复

代码修复有两大主要方案,一种是阿里系的底层替换方案,另一种是腾讯系的类加载方案。

两种方案各有优劣:

  • 底层替换方案限制颇多,但时效性最好,加载轻快,立即见效。
  • 类加载方案时效性差,需要重新冷启动才能见效,但修复范围广,限制少。
底层替换方案是在已经加载了的类中直接替换掉原有的方法,是在原来类的基础上进行修改。无法实现对原有类方法和字段的增减。
底层替换最为人狗命的地方是底层替换的不稳定性。

类加载方案的原理是app重新启动后让ClassLoader去加载新的类。在app运行过程中,所有需要发生变更的类已经被加载过了,在Android上无法对一个类进行卸载的。

如果不重启,原来的类还在虚拟机中,就无法加载新类。
因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会使用新类,从而达到热修复的目的。

说说Tinker:

微信的Tinker方案是完整的全量dex加载,并且可谓是将补丁合成做到了极致,然而我们发现,精密的武器并非适用于所有战场。

Tinker的合成方案,是 从dex的方法和指令维度 进行全量合成,整个过程都是自己研发。

虽然可以很大的节省空间,但对于dex内容的比较粒度过细,实现较为复杂,性能消耗严重。

实际上,dex的大小占整个apk的比例是比价低的,一个app里面的dex文件大小并不是主要部分,而占空间大的主要还是资源文件。

因此,Tinker方案的时空代价转换的性价比不高。

其实,dex比较的最佳粒度,应该是在类的维度。它即不像方法和指令维度那样的细微,也不像bsbiff比较那样的粗糙。在类的维度,可以达到时间和空间平衡的最佳效果。


既然两种方案各有其特点,把他们联合起来就是最好的选择了。

Sophix的代码修复体系正式同时涵盖了两种方案。在补丁生成阶段,补丁工具会根据实际代码的变动情况进行自动选择:

  • 针对小修改,在底层替换方案限制范围内,就直接采用底层替换修复。
  • 对于代码修复超出底层替换限制的,会使用类加载替换,虽然及时性没有那么好,但总归可以达到热修复的目的。

另外,在运行时还会再判断所运行的机型是否支持热修复,这样即使机型底层虚拟机构造不支持,还是会走类加载修复,从而达到最好的兼容性。

2.1底层热替换原理

AndFix:由补丁类的classLoader加载补丁类,在native层针对不同Android架构中的不同的ArtMethod结构调用对应的replaceMethod方法按照定义好的ArtMethod结构一一替换方法的所有信息如所属类、访问权限、代码内存地址等。
稳定性较差,会受到国内ROM厂商对ArtMethod结构更改的影响,所以这正是AndFix不支持很多机型的原因。

Sophix:由补丁类的classLoader加载补丁类,在native层直接memcpy(smeth,dmth,sizeof(ArtMethod))替换整个artMethod的结构。初始化类时会为这个类分配空间,AllocArtMethodArray会紧挨着的new出来放入art中的方法数组中。通过计算辅助类的前后两个方法的起始地址就可以计算出artMethod结构的大小了。
注:补丁类初始化时,也会分配自己的artMethod空间,拿这个修复过的新ArtMethod去替换旧ArtMethod的内容,不用管ArtMethod的结构。稳定性大大提高!

猜测:由于补丁类加载是从dex中加载,故替换后的ArtMethod的方法入口首先应该是dexCode解释执行,同步被优化成oat机器码,下次执行时就执行oat机器码入口了。

  • 访问权限问题
    1.方法调用时权限检查
    得益于在安装时优化成oat文件时,已经校验过了。所以对同类的方法进行调用时,不会再进行权限检查。
    2.同包名下权限问题
    同包名下调用热修复之后的方法,会再次权限检查,在native中的IsInSamePackage方法中判断两个类的classLoader是否相同,否则IllegalAcessError,因为补丁类是由补丁classLoader加载的,所以解决方法时,反射修改加载之后的补丁类的classLoader字段为旧classLoader。
  • 反射调用非静态方法产生的问题(依赖冷启动解决)
    反射调用非静态方法时,会调用底层的InvokeMethod,会调用VerifyObjectIsClass来判断调用方法的对象是否是方法所属类的实例,然而由于热替换的方法类还是执行补丁类,所以校验失败。
  • 即时生效带来的限制
    两种情况不适用,仅支持修复方法,其他情况补丁小,修复快。
    1.引起了原有类中结构变化的修改
    2.修复的非静态方法被反射调用
底层替换方案
  • 原理:在已经加载的类中直接替换掉原有方法,是在原有类的结构基础上进行修改的。在hook方法入口ArtMethod时,通过构造一个新的ArtMethod实现替换方法入口的跳转。
  • 应用:能即时生效,Andfix采用此方案。
  • 缺点:底层替换稳定性不好,适用范围存在限制,通过改造代码绕过限制既不优雅也不方便,并且还没提供资源及so的修复。

资源修复

目前市面上的很多热修复方案基本上都参考了Instant Run的是实现。

简单来说,Instant Run中的资源热修复方案分为两步:

  1. 构造一个新的AssetManager,并通过反射调用addAssetPath,把这个完整的新资源包加入到AssetManager中,这样就得到了一个含有所有新资源的AssetManager。
  2. 找到所有之前引用到原有AssetManager的地方,通过反射,把引用处替换为新的AssetManager。

其实,在该方案中有大量的代码都是在处理兼容性问题和找到所有AssetManager的引用。真正替换的代码其实很简单。

Sophix并没有直接采用Instant Run技术,而是构造了一个package id为0x66的资源包,其实这个资源包里面只有修改了的资源项,直接在原有的AssetManager中addAssetPath就可以了。

由于补丁包的Package Id为0x66,不与目前已经加载的0x7f资源段冲突,因此直接加入到已有的AssetManager中就可以直接使用了。

2.2你所不知道的Java

类加载方案
  • 原理:让app重新启动后让ClassLoader去加载新的类。如果不重启,原来的类还在虚拟机中无法重复加载。

  • 优点:修复范围广,限制少。

  • 应用:腾讯系包括QQ空间,手QFix,Tinker采用此方案。QQ空间会侵入打包流程。QFix需要获取底层虚拟机的函数,不稳定。Tinker是完整的全量dex加载。

dex的大小占整个apk比例较低,一个app里的dex文件大小不是主要部分,占空间大的主要是资源文件。

新美高梅网站 2冷启动主流框架分析

  • QQ空间的插桩原理将一个单独无关版主类放到一个单独的dex中,原dex中所有类的构造函数都引用这个类,一般的实现方法都是侵入dex打包流程,利用.class字节码修改技术,在所有.class文件的构造函数中引用这个帮助类。

  • Tinker与Sophix方案不同之处Tinker采用dex merge生成全量DEX方案。反编译为smali,然后新apk跟基线apk进行差异对比,最后得到补丁包。Dalvik下Sophix和Tinker相同,在Art下,Sophix不需要做dex merge,因为Art下本质上虚拟机已经支持多dex的加载,要做的仅仅是把补丁dex作为主dex(classes.dex)加载而已:将补丁dex命名为classes.dex,原apk中的dex依次命名为classes(2, 3, 4...).dex就好了,然后一起打包为一个压缩文件。然后DexFile.loadDex得到DexFile对象,最后把该DexFile对象整个替换旧的dexElements数组就好了。

    新美高梅网站 3Tinker与Sophix方案不同点

DexFile.loadDex流程DexFile.loadDex尝试把一个dex文件解析并加载到native内存,在加载到native内存之前,如果dex不存在对应的odex,那么Dalvik下会执行dexopt,Art下会执行dexoat,最后得到的都是一个优化后的odex。实际上最后虚拟机上执行的是这个odex而不是dex。

新美高梅网站 4dexopt流程

基本参考InstantRun的实现:构造一个包含所有新资源的新的AssetManager。并在所有之前引用到原来的AssetManager通过反射替换掉。Sophix不修改AssetManager的引用,构造的补丁包中只包含有新增或有修改变动的资源,在原AssetManager中addAssetPath这个包就可以了。资源包不需要在运行时合成完整包。

本质是对native方法的修复和替换。类似类修复反射注入方式,将补丁so库的路径插入到nativeLibraryDirectories数据最前面。

热修复主流框架对比可查阅Android热修复主流框架调研

SO库修复

SO库的修复本质上是对native方法的修复和替换。

我们采用的是类似 类修复反射注入方式 。把补丁so库插入到nativeLibraryDirdetories数组的最前面。就能够达到加载so库的时候是补丁so库,而不是原来so库的目录,从而达到修复的目的。

采用这种方案,完全有Sophix启动期间反射注入pathc中的so库。对开发者透明。

不用像其他方案那样需要手动的替换系统的System.load来实现替换目的。

内部类编译

  • 静态内部类/非静态内部类区别
    内部类会被编译器生成同外部类一样的顶级类。只不过非静态内部类会持有外部类的引用。这也是Android性能优化建议Handler使用静态内部类,防止外部类Activity不能被回收导致造成OOM。
  • 内部类和外部类互相访问
    内部类和外部类互相访问private方法和字段时,会自动在对应类为对方生成public的access&**方法。
  • 热部署解决方案
    外部类如果有内部类把所有的field/method的private访问权限改成proteced或者public
    内部类将所有的field/method的private访问权限改成proteced或者public

代码修复技术详解

匿名内部类编译

  • 匿名内部类命名规则
    外部类&numble
    number即编译器根据匿名内部类出现在外部类中的顺序,依次累加。
  • 热部署解决方案
    新增/减少匿名内部类对热部署是无解的,因为补丁修复工具拿到的是class文件,无法区别DexFileDemo&1和DexFileDemo&2,会导致类的顺序乱套。如果匿名内部类插入到末尾是允许。

底层替换原理


AndFix方案引发的思考

在各种Android热修复方案中,AndFix的即时生效令人印象深刻,它稍显另类,并不需要重新启动,而是在加载补丁后直接对方法进行替换就可以完成修复,然而它的使用限制也遭遇到更多的质疑。

怎么做到即时生效?
在app运行到一半的时候,所有需要发生变更的类已经被加载过了,在Android上是无法对一个类进行卸载的。AndFix采用的方法是,在已经加载了的类中直接在native层替换到原有方法,是在原来类的基础上进行修改的。

每一个Java方法在ART中都对应着一个ArtMethod,ArtMethod记录了这个Java方法的所有信息,包括所有类、访问权限、代码执行地址等等。

通过env->FromReflectedMethod,可以由Method对象得到这个方法对应的ArtMethod的真正其实地址。然后把它强转为ArtMethod支持,从而对所有成员进行修改。

这样全部替换完之后就完成了热修复逻辑。以后调用这个方法时就会直接走到新方法中了。


有趣的域编译

  • 静态field,非静态field编译
    热部署不支持field/method增加和删除和<clinit>方法的修改
    静态field的初始化和静态代码块会被编译在编译器合成的方法<clinit>中
    非静态字段的初始化会被编译在编译器生成的<init>无参构造函数中
  • 静态field,静态代码块
    <clinit>方法会在类加载阶段的类初始化时调用,<clinit>中静态field和静态代码块的出现顺序就是二者在源码中出现的顺序。因为类已经加载过了,所以就算修复了<clinit>方法也不会生效了。
    dvmResolveClass->dvmLinkClass->dvmInitClass,然后执行clinit方法
    以下情况会去加载一个类
    1.new 一个类的对象时new instance
    2.调用类的静态方法(invoke static)
    3.获取类的静态域的值(sget)
  • 非静态field,非静态代码块
    类的构造函数会被编译器翻译成<init>方法,会先进行非静态field和非静态代码块的初始化。它们出现的顺序也是和在源码中出现的顺序一样。
    执行new instance指令时,如果类没有加载过,就尝试加载类。然后对对象内存分配,再然后执行invoke direct指令调用类的init构造函数进行初始化
  • 热部署解决方案
    不支持对静态字段和静态代码块的修改,会导致热部署失败,只能冷启动生效。支持非静态字段和非静态代码块修改,热部署只是将init构造函数作为普通的方法变更。

虚拟机调用方法的原理分析

为什么替换ArtMethod数据后可以实现热修复呢?这需要从虚拟机调用方法的原理说起。

以Android6.0实现为例。

ArtMethod结构中最重要的两个字段entry_point_from_interprete_entry_point_from_quick_compiled_code_

ART中可以采用解释模式或者AOT机器码模式执行

解释模式:
就是取出Dex Code,逐条的解释执行就行了。如果方法的调用者是以解释模式运行的,在调用这个方法是,就去取得这个方法的`entry_potin_from_interpreter_`,然后跳转过去执行。

AOT模式:
预先编译好Dex Code对应的机器码,然后运行期直接执行机器码就行了,不需要一条条的解释执行Dex Code。如果方法的调用者是以AOT机器码执行的,在调用这个方法是,就是跳转到`entry_point_from_quick_compiled_code_`执行。

那我们是不是只需要替换这几个字段就可以了呢?

并没有这么简单。以为不论是解释模式还是AOT模式,在运行期间还会需要用到ArtMethod的里面的其他成员字段的。

其实这样正式native替换方式兼容性问题的原因。

Sophix没有选择将Artmethod的所有成员都进行替换,而是把ArtMethod作为整天进行替换。

美狮美高梅官方网站,这里需要强调的是求解ArtMethod结构的内存占用大小。

由于我们是在运行时生效(各家ROM都会有多多少少的改动),且sizeofsizeof()工作原理是在编译期,因此我们无法直接使用该表达式。

Sophix采用了比较聪明的办法:利用现行结构的特定,使用两个ArtMethod之前的偏移量来动态计算ArtMethod的数据结构大小。但这里需要依赖存放ArtMethod的数据结构是线性的。

final static 域编译

  • final static 域编译规则
    final static引用类型初始化仍在<clinit>中
    final static基本类型和String类型,类加载初始化dvminitClass在执行clinit方法之前,先执行initSFields,这个方法为static域赋予默认值。引用类型默认NULL,final static修饰的基本类型和String类型会在这里初始化赋值。
  • final static 域优化原理
    final static基本类型执行const/4指令,操作数在dex中的位置(encoded_array_item)就是在opcode后一个字节。
    final static String类型执行const-string指令,本质同上只不过拿到的是字符串常量在dex文件结构中字符串常量区的索引id。dex文件有一块区域存储所有的字符串常量会被完整的加载到虚拟机内存中-字符串常量区。
    final static引用类型执行sget指令,首先调用dvmDexGetResolveField看这个域是否之前解析过,没有的话调用dvmDexResolveField尝试解析域,如果这个静态域所在的类没有解析过,尝试调用dvmResolveClass,拿到这个sField,然后通过dvmDexGetResolveField(sField)获取这个静态值。
  • 热部署解决方案
    final static基本类型/string类型最终引用的类型会被热部署替换掉。
    final static引用类型因为会被翻译到clinit方法中,热部署失败。

替换后方法访问权限问题

1、类内部

上述提到,我们整个替换ArtMethod的内容,但新替换的方法的所属类和原来方法的所属类,是不同的类。

新美高梅网站,被替换的方法有权限访问其他的private方法吗?

通过观察Dex Code和Native Code,可以推测,在dex2oat生成AOT机器码时是有做一些检查和优化的,由于dex2oat编译机器码时确认了两个方法同属一个类,所以机器码中就不存在权限检查相关代码。

2、同包名下

但是并不是所有方法都如同类内部直接访问那样顺利的。

补丁类正在访问同包名下的类时会报出访问异常。

具体的校验逻辑是在虚拟机代码的Class::IsInSamePackage中,关键点在于比较两个Class的所属的ClassLoader。

因此这里还需要将新类的ClassLoader设置为与原来一致。

3、反射调用非静态方法

当一个非静态方法被热替换后,再反射调用这个方法,会抛出异常。

在反射Invoke一个方法时,在底层会掉哦用到InvokeMethod -> VerifyObejctIsClass函数做验证。

由于热替换方案锁替换的非静态方法,在反射调用者,由于VerifyObjectIsCLass时,旧类和新类不匹配,就会导致验证不通过。

如果是静态方法,会有同样的问题吗?

当然没有,静态方法会在类的级别直接进行调用的,不需要接受对象实例作为参数,不会有这方面的检查。

因此,对于这种反射调用非静态方法的问题,Sophix会采用另一种冷启动机制对付,最后会有介绍。

有趣的方法编译

  • 应用混淆方法编译
    如果项目应用了混淆,会导致方法内联和裁剪最终导致Method新增/减少
  • 方法内联
    以下几种情况下回发生方法内联(该方法会被删除)
    1.方法没有被任何地方引用
    2.方法仅在一处被引用,调用方法的地方会被方法的实现替换掉
    3.方法太简单,仅仅一行语句。调用的语句也会被其实现所替换掉
    如果补丁修复的方法突然调用了原先只有一处被调用的方法,那么原先被内联掉的方法会新增出来,导致热修复失败。
  • 方法裁剪(参数会被删除)
    方法的参数没有被引用过,该方法会被裁剪,然后再进行混淆。如果补丁方法再次调用这个参数就会导致新增方法,那么只能走冷启动方案。
    可以采用走包装类型Boolean判断,简单调用该参数,保持对该参数的引用就不会被裁剪了
  • 热部署解决方案
    只要混淆配置文件中-dontoptmize就不会做方法内联和裁剪了。所以不建议混淆时优化代码。
    而且因为Android执行的是优化后的dex文件,所以混淆中预校验在class文件中的优势就不存在了。

即时生效带来的限制

除了反射的问题,即时生效直接在运行期修改底层结构的热修复方法,都存在着一个限制,那就是只能替换方法。对于补丁里面如果存在方法的增加或者减少,以及成员字段的增加和减少情况都是不适用的。

原因是这样的,一旦补丁类中出现了方法的增减,会导致整个类以及整个dex方法数的变化。方法数的变化伴随着方法索引的变化,这样在访问时就无法所引导正确的方法了。

如果字段发生了增减,和方法变化差不多,所有字段的索引都会发生变化。

并且更加严重的是,如果程序运行中间某个类突然增加了一个字段,那么对于原来已经生成的实例,还是原来的结构,已是无法改变的了,而新方法在使用到老实例时,访问新增字段就会产生不可预期的结果。

因此综合来说,即时生效方案只有在下面两种情况下是不适用的:

  1. 引起了原有类发生结构变化的修改
  2. 修复了的非静态类会被反射调用

虽然有一些使用限制,但一旦满足使用条件,这种热修复方式还是十分出众的,补丁小,加载迅速,能够实时生效,无需重启app,并且有着完美的设备兼容性(整个copy Method结构)。

switch case语句编译

  • switch case语句编译规则
    编译器会根据switch case的值是否连续分别生成不同的指令,packed-switch和sparse-switch指令。如果packed有值不连续就用pswitch_0补齐 return-void。
  • 热部署解决方案
    在sophix进行资源补丁包时,需要对引用的资源进行替换,如果swith case语句恰好被编译成packed-switch指令则可能会漏掉。解决方法是修改打补丁包时的smail反编译流程,碰到packed-switch指令强转为sparse-switch指令,:pswitch_N等标签指令也需要被替换成:sswitch_N指令,然后做资源Id替换,编程smail为dex

Java语言的编译实现所带来的挑战

Sophix一直秉承 粒度小、注重快捷热修复、无侵入适合原生工程。因为这个原则,我们在研发过程中遇到很多 编译期 的问题,引用印象深刻。

泛型编译

  • 为什么需要泛型
    Java泛型完全有编译器实现,由编译器执行类型检查和类型推断,生成非泛型字节码,称之为擦除。
    没有泛型之前想要实现类泛型,利用所有类的父类时Object进行强转,这完全依赖程序员的自主性,很容易出现ClassCastException。泛型的出现解决了类型检查和类型推断的问题。
  • 泛型类型擦除
    Java字节码中不包含泛型类型信息,想要区别类型定义可以限定泛型类型 <T extends Number>
  • 类型擦除与多态的冲突和解决
    父类是泛型类有setNumber(T value),子类想override setNumber(Number value)。然而实际父类的方法实际是setNumber(Object value),子类想重写却变成了重载,这就出现了类型擦除和多态之间的冲突。然而编译器自动帮我们合成了Bridge方法实现了重载,在子类中生成了相同签名bridge方法,内部实际调用子类的重写方法。
  • 泛型类型转换
    编译器如果发现变量声明加上了泛型信息,编译器自动加上了check-cast的强制转换,因为编译器会为泛型做类型检查,所以自动的强制转换不会出现ClassCastException。
  • 热部署解决方案
    如果父类补丁变成了增加了泛型则会增加Bridge方法,造成热部署失败。
    将方法从void get(B t) 变成<B extends Number> void get(B t)方法逻辑不会发生变化,但是方法的签名会发生变化,这种情况热修复没有意义,需要避免这种情况的发生。

内部类

问题:有时候会发现,修改外部类某个方法逻辑为访问内部类的某个方法时,最后打包出来的补丁包竟然提示新增了一个方法。

因此我们很有必要了解内部类在编译期间是怎么编译的。

首先需要知道 ** 内部类会在编译期会被编译为跟外部类一样的顶级类。 **


静态内部类和非静态内部类的区别。

它们的区别其实大家都很熟悉,非静态类持有外部类的引用,静态内部类不持有外部类的引用。

既然内部类跟外部类一样都是顶级类,那是不是意味着对方private的method/field是没法被访问到的,事实上外部类为了访问内部类私有的域和方法,编译期会自动外内部类生成access&**相关方法。

因此,如果补丁类中修改的方法中添加了需要访问内部类私有数据或者方法的代码的话,那么编译期间会新增access&**方法,供内部类被访问使用。


如果想通过热部署修复的新方法需要访问内部类的私有域或方法,那么我们应该防止生成access&**相关方法。

Sophix有以下建议:

  • 外部类如果有内部类,把外部类所有的method/fidle的private访问权限修改为projected或者默认访问权限或者public。
  • 同时把内部类的所有的method/field的private访问修改为projected或者模式访问权限或者public。

Lambda表达式编译

  • Lambda表达式编译规则
    Lamda表达式具有函数式编程的特点,是Java中最接近闭包的概念。函数式接口:一个接口具有唯一一个抽象方法
    Java中的Runable和Comparator都是典型的函数式接口
    Lamada表达式和匿名内部类的区别:
    1.this关键字指包围Lamada表达式的类而不是指向匿名内部类自己
    2.编译方式,Java编译器将Lamda表达式编译成类的私有方法,使用了Java7的invokedynamic动态绑定这个私有方法。而匿名内部类则是生成外部类&number的新类
    编译器都会在类下生成lamda$main$**{ * }私有静态方法,这个方法实现了lamda表达式的逻辑,引用的变量都会变成方法的参数。
    在HostSpot VM下解释class文件的lamda表达式:
    invokeDynamic指令调用java/lang/invoke/LamdaMetafactory的metafactory这个静态方法。这个方法会在运行时生成实现函数式接口的具体类,这个具体类会调用那个静态私有方法。
    在Android虚拟机下解释dex文件中的lamda表达式:则是在优化成dex文件的时候就生成了这个具体类。
  • 热部署解决方案
    新增lamada表达式会导致外部类新增一个辅助方法。修改的lamda表达式逻辑引用了外部变量,会导致辅助类持有了外部对象,会新增这个外部对象的变量。也是会导致热修复失败。

匿名内部类

匿名内部类其实也是个内部类,自然也会有上一小节中说到的限制和应对策略。

但它还会有其他的限制。


匿名内部类的命名规则

匿名内部类顾名思义就是没有名字。

命名格式一般都是外部类&numble,后面的numble,是编译器根据匿名内部类在外部类中出现的先后关系,一次累加命名。


解决方案

新增/减少匿名内部类,实际上对于热部署来说是无解的,因为补丁工具拿到的已经是编译后的.class文件,所以根本无法区分,所以这种情况下,应极力避免插入一个新的匿名内部类。

当然,如果匿名内部类是插入到外部类的末尾,那么是允许的。(确是很容易犯错的)

访问权限检查对热替换的影响

  • 类加载阶段父类/实现接口访问检查
    一个类的加载阶段包括resolve->link->init三个阶段,父类/实现接口 权限检查在link阶段,dvmLinkClass中依次对父类/实现接口进行dvmCheckClassAcess权限检查,如果父类/实现接口是非public然后进行检查当前类和它是否是相同的ClassLoader。热修复补丁类是新classLoader加载的,所在会报父类不允许访问的错误。
  • 类校验阶段访问权限检查
    如果访问public类和方法在类加载阶段会通过,但是在运行时会爆出crash异常。
    补丁在单个dex文件中,加载dex肯定要进行dexopt,再dexopt过程中会dvmVerifyClass校验dex每个类。在校验过程中会检查补丁类所引用类的访问权限(提前dvmResolveClass被调用类)。还会校验调用方法的访问权限,public修饰直接返回。protected的话,先检查当前类和被调用方法所属类是否是父子类关系,不是的话会调用dvmIsSmaePackage,这里会判断是否是相同的classLoader。...

本文由美狮美高梅官方网站发布,转载请注明来源

关键词: