插件化和热修复技术是Android开发中比较高级的知识点,是中级开发人员通向高级开发中必须掌握的技能,插件化的知识可以查我我之前的介绍:Android插件化。本篇重点讲解热修复,并对当前流行的热修复技术做一个简单的总结。
插件化和热修复技术是Android开发中比较高级的知识点,是中级开发人员通向高级开发中必须掌握的技能,插件化的知识可以查我我之前的介绍:Android插件化。本篇重点讲解热修复,并对当前流行的热修复技术做一个简单的总结。
热修复
什么是热修复?
简单来讲,为了修复线上问题而提出的修补方案,程序修补过程无需重新发版!
技术背景
在正常软件开发流程中,线下开发->上线->发现bug->紧急修复上线。不过对于这种方式代价太大。
而热修复的开发流程显得更加灵活,无需重新发版,实时高效热修复,无需下载新的应用,代价小,最重要的是及时的修复了bug。
当前热门的热修复技术
当前热门的热修复技术有:
热修复技术
要弄清热修复技术的原理,就要先弄清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 域优化原理
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方法中,热部署失败。
泛型编译
为什么需要泛型
Java泛型完全有编译器实现,由编译器执行类型检查和类型推断,生成非泛型字节码,称之为擦除。
没有泛型之前想要实现类泛型,利用所有类的父类时Object进行强转,这完全依赖程序员的自主性,很容易出现ClassCastException。泛型的出现解决了类型检查和类型推断的问题。
本文为授权转载文章,任何人未经原授权方同意,不得复制、转载、摘编等任何方式进行使用,e-works不承担由此而产生的任何法律责任! 如有异议请及时告之,以便进行及时处理。联系方式:editor@e-works.net.cn tel:027-87592219/20/21。