Android 热修复方案选型
1.原理部分
1.1class&dex文件详解
什么是class文件?能够被 JVM 识别,加载并执行的文件格式。
class文件结构
什么是class文件?
能够被 JVM 识别,加载并执行的文件格式。
如何生成class文件?
IDE帮我们build。
javac生成class文件
javac Hello.java
。
class的作用
记录一个类文件的所有信息。
010editor查看二进制文件
结构
- 一种8字节的二进制流文件。
- 各个数据紧密排列无间隙。
- 每个类或者接口都单独占据一个class文件。
常量池constant_pool
存储索引
- CONSTANT_Integer_info
- CONSTANT_Long_info
- CONSTANT_String_info
- CONSTANT_Class_info
- CONSTANT_Fieldref_info
- CONSTANT_Methodref_info
access_flags
描述的是当前类(或者接口)的访问修饰符, 如public, private等
标志名 | 标志值 | 标志含义 | 针对的对像 |
---|---|---|---|
ACC_PUBLIC | 0x0001 | public类型 | 所有类型 |
ACC_FINAL | 0x0010 | final类型 | 类 |
ACC_SUPER | 0x0020 | 使用新的invokespecial语义 | 类和接口 |
ACC_INTERFACE | 0x0200 | 接口类型 | 接口 |
ACC_ABSTRACT | 0x0400 | 抽象类型 | 类和接口 |
ACC_SYNTHETIC | 0x1000 | 该类不由用户代码生成 | 所有类型 |
ACC_ANNOTATION | 0x2000 | 注解类型 | 注解 |
ACC_ENUM | 0x4000 | 枚举类型 | 枚举 |
class文件弊端:
- 内存占用大不适合移动端。
- 堆栈加栈模式,加载速度慢
- io操作多,类查找慢。
dex文件结构
什么是dex文件
能够被 DVM 识别,加载并执行的文件格式。
如何生成一个dex文件
IDE自动build生成
dx命令生成dex文件
javac -target 1.6 -source 1.6 Hello.java
dx --dex --output Hello.dex Hello.class
手动运行dex文件在手机
adb push Hello.dex /storage/emulate/0
dex文件的作用
记录整个工程所有类文件的信息
dex文件格式详解
- 一种8位字节的二进制流文件。
- 各个数据按顺序紧密的排列,无间隙。
- 整个应用中所有Java源文件都放在一个 dex 中。
class与dex文件对比
- 本质上都是一样的,dex是从 class文件演变而来。
- class 文件存在很多冗余信息,dex 会去除冗余,并整合(所有内容存在一个数据取)。
1.2 虚拟机
Java虚拟机结构解析
JVM整体结构
Java代码的编译和执行过程
类加载器:java转class(可执行)文件classloader。
加载流程:
- Loading:类信息从文件中获取并载入到JVM内存。
- Verifying:检查读入的结构是否符合JVM规范的描述。
- Preparing: 分配一个结构用来存储类信息。
- Resolving:把这个类的常量池中的符号引用改变成直接引用。
- Initalizing:执行静态初始化程序,把静态变量初始化。
内存管理
java栈区:存放java方法执行的时候的所有数据。
组成: 由栈帧组成,一个栈代表一个方法的执行。
栈帧每个方法聪调用到执行完成就对应一个栈帧在虚拟机栈中入栈到出栈。
组成:局部变量表、栈操作数、动态链接、方法出口。
本地方法栈:专门为native方法提供服务的,和java栈区一样。
方法区:(永远占据内存,存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后等数据)。
堆区:所有通过new创建的对象的内存都在堆中分配。
特点:是虚拟机中最大的一块内存,是GC要回收的部分。
堆区内存:分新生代和老生代内存区域,可以动态分配。
垃圾回收
垃圾收集算法
引用记数算法(可能会互相引用,难以回收)
可达性算法
寻找不被引用的节点
引用类型:
强,软,弱,虚。
最常用就是强和弱
弱引用创建
Object obg=new Object();//强
WeakReference<Object>wf=new WeakReference<Object>(obj);//弱
obj=null;
wf.get();//获取到弱引用,可能为空,因为不防回收
垃圾回收算法
标记-清除算法
复制算法
标记整理
触发回收
Java虚拟机无法为新对象分配内存空间了。
手动调用System.gc() (强烈不推荐)
低优先级的GC线程,被运行就会执行GC
Dalvik和JVM的不同
- 执行文件不同,一个class,一个是dex
- 类加载的系统和jvm区别大
- 同时存在多个DVM
- Dalvik是基于寄存器的,JVM是基于栈的。
ART比Dalvik有哪些优势
- DVM用JIT将字节码转机器码,效率低。
- ART用AOT预编译技术,执行速度更快。
- ART会占用更多的应用安装事件和存储空间。
1.3 ClassLoader
Java 中的ClassLoader
AndroidClassLoader
种类
BootClassLoader 加载系统类库
PathClassLoader 加载已经安装好apk
DexClassLoader 指定要加载文件路径的(动态加载核心依据)
BaseDexClassLoader
至少要BootCLassLoader
PathClassLoader App才能使用
特点作用
双亲代理模式:一个类先看看有没有被classloader加载过,如果没有,看看有么有被父classloader加载,都没有才会加载。这样不会重复加载。
类加载的共享功能,classloader加载一次就可以公用。
类加载的隔离功能,加载的类肯定都不一样,防止被自定义的类冒充系统的类,更安全。
加载流程
ClassLoader: loadClass();
BaseDexClassLoader :findClass()
DexPathList :findClass()
DexFile:loadClassBinaryName()
动态加载的难点
- 许多组件要注册才能使用(Activity)。
- 资源的动态加载很复杂(资源也要注册)。
- 适配难。
Android程序运行时需要一个上下文环境。
提供上下文环境就是热修复的关键。
2. 热修复常见技术方案对比
基本概念:动态修复App。
优点:
- 刚发布的应用就发生了严重bug。
- 发布一些小功能给用户使用 。
缺陷:
- 市面上的热修复都存在一些兼容问题。
- 热修复也需要像正式发布一样处理版本和测试问题,不能过度依赖。
流行方案
- QQ空间超级补丁
- 微信Tinker
- 阿里AndFix,dexposed
- 美团Robust,lele的migo,百度hotfix
AndFix和QQ空间补丁最早出现,Tinker功能最齐全也最复杂。
技术对比
当前市面的热补丁方案有很多,其中比较出名的有阿里的AndFix、美团的Robust以及QZone的超级补丁方案。但它们都存在无法解决的问题,这也是正是我们推出Tinker的原因。
Tinker | QZone | AndFix | Robust | |
---|---|---|---|---|
类替换 | yes | yes | no | no |
So替换 | yes | no | no | no |
资源替换 | yes | yes | no | no |
全平台支持 | yes | yes | yes | yes |
即时生效 | no | no | yes | yes |
性能损耗 | 较小 | 较大 | 较小 | 较小 |
补丁包大小 | 较小 | 较大 | 一般 | 一般 |
开发透明 | yes | yes | no | no |
复杂度 | 较低 | 较低 | 复杂 | 复杂 |
gradle支持 | yes | no | no | no |
Rom体积 | 较大 | 较小 | 较小 | 较小 |
成功率 | 较高 | 较高 | 一般 | 最高 |
总的来说:
- AndFix作为native解决方案,首先面临的是稳定性与兼容性问题,更重要的是它无法实现类替换,它是需要大量额外的开发成本的;
- Robust兼容性与成功率较高,但是它与AndFix一样,无法新增变量与类只能用做的bugFix方案;
- Qzone方案可以做到发布产品功能,但是它主要问题是插桩带来Dalvik的性能问题,以及为了解决Art下内存地址问题而导致补丁包急速增大的。
特别是在Android N之后,由于混合编译的inline策略修改,对于市面上的各种方案都不太容易解决。而Tinker热补丁方案不仅支持类、So以及资源的替换,它还是2.X-8.X(1.9.0以上支持8.X)的全平台支持。利用Tinker我们不仅可以用做bugfix,甚至可以替代功能的发布。
AndFix作为native解决方案,首先面临的是稳定性与兼容性问题,更重要的是它无法实现类替换,它是需要大量额外的开发成本的。
所以最终我们采用Tinker做为热修复方案。
Tinker的使用
Tinker是什么
Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。
它主要包括以下几个部分:
- gradle编译插件:
tinker-patch-gradle-plugin
- 核心sdk库:
tinker-android-lib
- 非gradle编译用户的命令行版本:
tinker-patch-cli.jar
核心原理
基于android的原生ClassLoader,开发了自己的ClassLoader。
基于原生的aapt开发了自己的aapt。
基于Dex文件的格式,研发了DexDiff算法(比较两个apk的dex文件,生成patch文件)。
集成
gradler中添加Tinker依赖
代码中完成Tinker的初始化