Tinker热修复

Version:1.0 StartHTML:0000000179 EndHTML:0000127578 StartFragment:0000040205 EndFragment:0000127538 SourceURL:file:///Y:\Desktop\学习\新建文件夹\Tinker热修复.docx

Tinker热修复

热修复背景

  1. 刚发布的版本出现了严重的Bug,这就需要去解决Bug、测试打包重新发布,这会耗费大量的人力和物力,代价比较大
  2. 已经更正了此前发布版本的Bug,如果下个版本是大版本,那么两个版本之间间隔时间会很长,这样要等到下个大版本发布再修复Bug,而之前版本的Bug会长期的影响用户
  3. 版本升级率不高,并且需要长时间来完成版本迭代,前版本的Bug就会一直影响不升级的用户
  4. 有一些小但是很重要的功能需要在短时间内完成版本迭代,比如节日活动

正常开发流程与热修复开发流程对比

热修复框架分类与对比

  1. 分类

阿里系:AndFix、Dexposed、阿里百川、Sophix

腾讯系:微信的Tinker、QQ空间的超级补丁、手Q的QFix

知名公司:美团的Robust、饿了么的Amigo、美丽说蘑菇街的Aceso

其它:RocooFix、Nuwa、AnoleFix

  1. 对比

代码修复

  1. 底层替换方案(代表例子:阿里系的Andfix)

在已加载的类中直接替换原有方法,是在原有类的基础上进行修改,无法实现对原有类进行方法和字段的增减,这样会破坏原有类的结构

最大问题是不稳定性,直接修改虚拟机方法实体的具体字段来实现的。Android是开源的,不同的手机厂商开源对代码进行修改,所以像Andfix就会出现在部分机型上的热修复失效的现象

  1. 类加载方案

APP重新启动后,让ClassLoader去加载新的类。(这里解释一下,每一个apk打包之后都会包含dex文件,而dex文件加载之后就是我们就是我们java中的class文件)

热修复优势

  1. 无需重新发布新版本,省时省力
  2. 用户无感知修复,也无需下载最新应用,代价小
  3. 修复成功率高,把损失降到最低

插桩原理

    什么叫插桩?其实很简单,就是一个插队的行为,在我们的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分包

 

分包的原因

  1. 65536限制

应用的方法数超过了最大数65536个。因为DVM Bytecode的限制,DVM指令集的方法调用指令invoke-kind索引为16bits,最多能引用65535个方法所以就导致在打包apk的时候每一个dex包里面最多只能存在65536个方法。

  1. LinearAlloc限制

在安装应用时可能会提示INSTALL_FAILED_DEXOPT,产生的原因就是LinearAlloc限制,DVM中的LinearAlloc是一个固定的缓存区,当方法数超出缓存区的大小时会报错。

分包原理

为了解决65536限制和LinearAlloc限制,从而产生了Dex分包机制。

Dex分包方案主要做的时在打包时将应用代码分成多个Dex,将应用启动时必须用到的类和这些类的直接引用类放到主Dex中,其它代码放到次Dex中。当应用启动时先加载主Dex,等到应用启动后再动态地加载次Dex,从而缓解了主Dex的65536限制和LinearAlloc限制

 

分包方法

  1. 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'

}

 

  1. 配置文件multidex.keep

一般就将我们第一个加载的activity以及我们的baseactivity、baseapplication放进去。(这里注意,实际的开发中,里面放的是加载第一个activity需要的class文件)

包名/BaseActivity.class

包名/BaseApplication.class

包名/dn/lsn16_demo/MainActivity.class

 

  1. Application重写attachiBaseContext

@Override

protected void attachBaseContext(Context base) {

         super.attachBaseContext(base);

         MultiDex.install(this);

}

 

Tinker热修改的实现

  1. 从服务器下载dex文件
  2. 如果修复包存在先删除
  3. 拷贝到私有目录
  4. 开始修复
    1. 创建自己的类加载器

    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);

        }

    }

 

    1. 获取系统的PathClassLoader

        PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();

 

    1. 获取自己的dexElements

// 获取自己的DexElements数组对象,传入我们自己创建的类加载器

Object myDexElements = ReflectUtils.getDexElements(

            ReflectUtils.getPathList(myClassLoader));

 

    1. 获取系统的dexElements

 // 获取系统的DexElements数组对象   ,传入通过context获取的系统的类加载器      

 Object sysDexElements = ReflectUtils.getDexElements(

                    ReflectUtils.getPathList(pathClassLoader));

 

    1. 将系统的dexElements和自己的合并成新的dexElements

// 合并,将两个DexElements数组对象对象合并为一个,注意,合并的原理就是创建一个新的DexElements数组对象,然后依次将我们自己的DexElements数组对象放进去,然后放置系统的DexElements数组对象

Object dexElements = ArrayUtils.combineArray(myDexElements, sysDexElements);

 

    1. 重新赋值给系统的pathList

// 获取系统的 pathList

Object sysPathList = ReflectUtils.getPathList(pathClassLoader);

 // 将合并的DexElements数组对象对象重新赋值给系统的 pathList

ReflectUtils.setField(sysPathList, sysPathList.getClass(), dexElements);

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值