Version:1.0 StartHTML:0000000179 EndHTML:0000127578 StartFragment:0000040205 EndFragment:0000127538 SourceURL:file:///Y:\Desktop\学习\新建文件夹\Tinker热修复.docx
Tinker热修复
热修复背景
- 刚发布的版本出现了严重的Bug,这就需要去解决Bug、测试打包重新发布,这会耗费大量的人力和物力,代价比较大
- 已经更正了此前发布版本的Bug,如果下个版本是大版本,那么两个版本之间间隔时间会很长,这样要等到下个大版本发布再修复Bug,而之前版本的Bug会长期的影响用户
- 版本升级率不高,并且需要长时间来完成版本迭代,前版本的Bug就会一直影响不升级的用户
- 有一些小但是很重要的功能需要在短时间内完成版本迭代,比如节日活动
正常开发流程与热修复开发流程对比
热修复框架分类与对比
- 分类
阿里系:AndFix、Dexposed、阿里百川、Sophix
腾讯系:微信的Tinker、QQ空间的超级补丁、手Q的QFix
知名公司:美团的Robust、饿了么的Amigo、美丽说蘑菇街的Aceso
其它:RocooFix、Nuwa、AnoleFix
- 对比
代码修复
- 底层替换方案(代表例子:阿里系的Andfix)
在已加载的类中直接替换原有方法,是在原有类的基础上进行修改,无法实现对原有类进行方法和字段的增减,这样会破坏原有类的结构
最大问题是不稳定性,直接修改虚拟机方法实体的具体字段来实现的。Android是开源的,不同的手机厂商开源对代码进行修改,所以像Andfix就会出现在部分机型上的热修复失效的现象
- 类加载方案
APP重新启动后,让ClassLoader去加载新的类。(这里解释一下,每一个apk打包之后都会包含dex文件,而dex文件加载之后就是我们就是我们java中的class文件)
热修复优势
- 无需重新发布新版本,省时省力
- 用户无感知修复,也无需下载最新应用,代价小
- 修复成功率高,把损失降到最低
插桩原理
什么叫插桩?其实很简单,就是一个插队的行为,在我们的apk加载的过程中,将我们修改之后打包的dex文件插队到放到DexPathList对象的dexElements这个数组中。这个时候,加载的dex文件的时候就会先加载我们放进去插队的dex,假如这个dex中存在class A,那么后续的dex文件中如果也存在class A,是不会被加载的。正是因为这个原因,我们利用这个原理进行热修复。
libcore/dalvik/src/main/java/dalvik/system/BaseDexClassLoader.java
//初始化DexClassLoader的时候会传入dexPach public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent, boolean isTrusted) { super(parent);
//这里会初始化pathList,传入dexPath this.pathList = new DexPathList(this, dexPath, librarySearchPath, null, isTrusted); if (reporter != null) { reportClassLoaderChain(); } } |
libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory, boolean isTrusted) { ……………………………………… //在这个方法里面,我们只关心这一句,通过传入的dexpath初始化了dexElements,这个变量热修复需要修改的变量 this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext, isTrusted); …………………………………….. } |
Dex分包
分包的原因
- 65536限制
应用的方法数超过了最大数65536个。因为DVM Bytecode的限制,DVM指令集的方法调用指令invoke-kind索引为16bits,最多能引用65535个方法所以就导致在打包apk的时候每一个dex包里面最多只能存在65536个方法。
- LinearAlloc限制
在安装应用时可能会提示INSTALL_FAILED_DEXOPT,产生的原因就是LinearAlloc限制,DVM中的LinearAlloc是一个固定的缓存区,当方法数超出缓存区的大小时会报错。
分包原理
为了解决65536限制和LinearAlloc限制,从而产生了Dex分包机制。
Dex分包方案主要做的时在打包时将应用代码分成多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到主Dex中,其它代码放到次Dex中。当应用启动时先加载主Dex,等到应用启动后再动态地加载次Dex,从而缓解了主Dex的65536限制和LinearAlloc限制
分包方法
- gradle配置
android { compileSdkVersion 28 defaultConfig { ... // 开启分包 multiDexEnabled true // 设置分包配置文件 multiDexKeepFile file('multidex.keep') } ... dexOptions { javaMaxHeapSize "4g" preDexLibraries = false additionalParameters = [ // 配置multidex参数 '--multi-dex', // 多dex分包 '--set-max-idx-number=50000', // 每个包内方法数上限 '--main-dex-list=' + '/multidex-config.txt', // 打包到主classes.dex的文件列表 '--minimal-main-dex' ] } } dependencies { ... implementation 'com.android.support:multidex:1.0.3' } |
- 配置文件multidex.keep
一般就将我们第一个加载的activity以及我们的baseactivity、baseapplication放进去。(这里注意,实际的开发中,里面放的是加载第一个activity需要的class文件)
包名/BaseActivity.class 包名/BaseApplication.class 包名/dn/lsn16_demo/MainActivity.class |
- Application重写attachiBaseContext
@Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); MultiDex.install(this); } |
Tinker热修改的实现
- 从服务器下载dex文件
- 如果修复包存在先删除
- 拷贝到私有目录
- 开始修复
- 创建自己的类加载器
private static void createDexClassLoader(Context context, File fileDir) { …………………………….. DexClassLoader classLoader; for (File dex : loadedDex) { //初始化类加载器 classLoader = new DexClassLoader(dex.getAbsolutePath(), optimizedDirectory, null, context.getClassLoader()); //热修复 hotFix(classLoader, context); } } |
-
- 获取系统的PathClassLoader
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader(); |
-
- 获取自己的dexElements
// 获取自己的DexElements数组对象,传入我们自己创建的类加载器 Object myDexElements = ReflectUtils.getDexElements( ReflectUtils.getPathList(myClassLoader)); |
-
- 获取系统的dexElements
// 获取系统的DexElements数组对象 ,传入通过context获取的系统的类加载器 Object sysDexElements = ReflectUtils.getDexElements( ReflectUtils.getPathList(pathClassLoader)); |
-
- 将系统的dexElements和自己的合并成新的dexElements
// 合并,将两个DexElements数组对象对象合并为一个,注意,合并的原理就是创建一个新的DexElements数组对象,然后依次将我们自己的DexElements数组对象放进去,然后放置系统的DexElements数组对象 Object dexElements = ArrayUtils.combineArray(myDexElements, sysDexElements); |
-
- 重新赋值给系统的pathList
// 获取系统的 pathList Object sysPathList = ReflectUtils.getPathList(pathClassLoader); // 将合并的DexElements数组对象对象重新赋值给系统的 pathList ReflectUtils.setField(sysPathList, sysPathList.getClass(), dexElements); |