返回首页
当前位置: 主页 > 网络编程 > Android教程 >

Android热修复技术总结

时间:2018-01-09 23:46来源:2018年最新注册送彩金www.zhixing123.cn 编辑:麦田守望者

插件化和热修复技术是Android开发中比较高级的知识点,是中级开发人员通向高级开发中必须掌握的技能,插件化的知识可以查我我之前的介绍:Android插件化。本篇重点讲解热修复,并对当前流行的热修复技术做一个简单的总结。

热修复

什么是热修复?

简单来讲,为了修复线上问题而提出的修补方案,程序修补过程无需重新发版!

技术背景

在正常软件开发流程中,线下开发->上线->发现bug->紧急修复上线。不过对于这种方式代价太大。

 

 

 

而热修复的开发流程显得更加灵活,无需重新发版,实时高效热修复,无需下载新的应用,代价小,最重要的是及时的修复了bug。

 

 

 

当前热门的热修复技术

当前热门的热修复技术有:

  • QQ空间超级补丁、微信[Tinker]
  • 阿里的Sophix、阿里Hotfix
  • 饿了么Amigo
  • 美团Robust
  • 360RePlugin

热修复技术

要弄清热修复技术的原理,就要先弄清Android的ClassLoader机制,相关文章可以阅读之前的介绍:ClassLoader类加载机制。Android的ClassLoader分为PathClassLoader和DexClassLoader,它们都都继承自BaseDexClassLoader,其中PathClassLoader用来加载系统类和应用类;DexClassLoader用来加载jar、apk、dex文件。例如下面要介绍的阿里的Andfix和Sophix的原理如下:

AndFix

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

Sophix

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

java

内部类编译

静态内部类/非静态内部类区别

内部类会被编译器生成同外部类一样的顶级类。只不过非静态内部类会持有外部类的引用。这也是Android性能优化建议Handler使用静态内部类,防止外部类Activity不能被回收导致造成OOM。

内部类和外部类互相访问

内部类和外部类互相访问private方法和字段时,会自动在对应类为对方生成public的access&**方法。

热部署解决方案

外部类如果有内部类把所有的field/method的private访问权限改成proteced或者public内部类将所有的field/method的private访问权限改成proteced或者public。

匿名内部类编译

匿名内部类命名规则

外部类&number。number即编译器根据匿名内部类出现在外部类中的顺序,依次累加。

热部署解决方案

新增/减少匿名内部类对热部署是无解的,因为补丁修复工具拿到的是class文件,无法区别DexFileDemo&1和DexFileDemo&2,会导致类的顺序乱套。如果匿名内部类插入到末尾则是允许。

域编译

静态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构造函数作为普通的方法变更。

final static 域编译

final static 域编译规则

final static引用类型初始化仍在clinit中final static基本类型和String类型,类加载初始化dvminitClass在执行clinit方法之前,先执行initSFields,这个方法为static域赋予默认值。引用类型默认NULL,final static修饰的基本类型和String类型会在这里初始化赋值。

final static 域优化原理

inal 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方法中,热部署失败。

泛型编译

为什么需要泛型

Java泛型完全有编译器实现,由编译器执行类型检查和类型推断,生成非泛型字节码,称之为擦除。
没有泛型之前想要实现类泛型,利用所有类的父类时Object进行强转,这完全依赖程序员的自主性,很容易出现ClassCastException。泛型的出现解决了类型检查和类型推断的问题。

泛型类型擦除

Java字节码中不包含泛型类型信息,想要区别类型定义可以限定泛型类型

类型擦除与多态的冲突和解决

父类是泛型类有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)方法逻辑不会发生变化,但是方法的签名会发生变化,这种情况热修复没有意义,需要避免这种情况的发生。

Lambda表达式编译

Lambda表达式编译规则

Lamda表达式具有函数式编程的特点,是Java中最接近闭包的概念。函数式接口:一个接口具有唯一一个抽象方法
Java中的Runable和Comparator都是典型的函数式接口

Lamada表达式和匿名内部类的区别:
1.this关键字指包围Lamada表达式的类而不是指向匿名内部类自己
2.编译方式,Java编译器将Lamda表达式编译成类的私有方法,使用了Java7的invokedynamic动态绑定这个私有方法。而匿名内部类则是生成外部类&number的新类.编译器都会在类下生成lamdamain*{ }私有静态方法,这个方法实现了lamda表达式的逻辑,引用的变量都会变成方法的参数。

在HostSpot VM下解释class文件的lamda表达式:
invokeDynamic指令调用java/lang/invoke/LamdaMetafactory的metafactory这个静态方法。这个方法会在运行时生成实现函数式接口的具体类,这个具体类会调用那个静态私有方法。
在Android虚拟机下解释dex文件中的lamda表达式:则是在优化成dex文件的时候就生成了这个具体类。

热部署解决方案

新增lamada表达式会导致外部类新增一个辅助方法。修改的lamda表达式逻辑引用了外部变量,会导致辅助类持有了外部对象,会新增这个外部对象的变量。也是会导致热修复失败。

Sophix与QQ超级补丁和Tinker技术比较

针对现在市面上比较流行的热修复方案,这里选择Sophix、QQ超级补丁和Tinker进行简单的介绍。前面说过,类似于qq空间和微信的实现方式都需要重新启动才能修复bug,而阿里的Sophix采用的是非浸入式的方式不需要冷启动。

QQ空间超级补丁

QQ空间超级补丁采用的插桩方式,入侵打包流程,单独放一个帮助类在独立的dex中让其他类调用,阻止类在dexopt时被打伤CLASS_ISPREVERIFIED标记。其原理如下图:

 


加载补丁dex得到dexFile对象作为参数构建一个Element对象插入到dexElement数组最前面。
Tinker提供差量包,整体替换dex的方案。将patch.dex与应用的class.dex合并生成一个完整的dex,加载完整的dex得到dexFile对象为参数构建一个Element对象替换dexElements数组。
官方multiDex没有补丁查询更新,下载补丁待下次启动时生效。
其流程可以总结为如下图所示:

顶一下
(2)
100%
踩一下
(0)
0%
标签(Tag):Android Android热修复技术
------分隔线----------------------------
------分隔线----------------------------
发表评论
请自觉遵守互联网相关的政策法规,严禁发布色情、暴力、反动的言论。
评价:
表情:
验证码:点击我更换图片
猜你感兴趣
博聚网