美文网首页Android开发经验谈Android高级技术
性能优化——APP 稳定性之热修复原理探索

性能优化——APP 稳定性之热修复原理探索

作者: df556ada620a | 来源:发表于2019-06-17 22:14 被阅读11次

    作者:DevYK
    链接:https://juejin.im/post/5cfce989f265da1b6c5f6991

    先扔效果图

    热修复的背景

    • 刚发布的版本出现了严重的 bug ,需要开发者去解决 bug,然后在测试打包重新发布,这会耗费大量的人力,物力,代价比较大。
    • 如果当前的 bug 不影响用户使用也不会崩溃,但是了下个版本是大版本,那么两个版本之间间隔时间会很长,这样要等到下个大版本发布在修复 bug , 而之前版本的 bug 还存在,虽说不影响使用,但是是一个潜在的 bug。
    • 版本升级率不高,并且需要长时间来完成版本迭代,前版本的 bug 就会一直影响不升级的用户。
    • 有一些小但是很重要的功能需要在短时间内完成版本迭代,比如假日活动。

    ..等等, 这里只是拿几个常见的举例说明。

    热修复的效率

    热修复框架对比

    框架名称 所属公司 是否开源 修复方式
    Dexposed alibaba 开源 实时修复
    Andfix alibaba 开源 实时修复
    Hotfix alibaba 暂未开源 实时修复
    Qzone 超级补丁 QQ 空间 暂未开源 冷启动修复
    QFix 手 Q 团队 开源 冷启动修复
    Robust 美团 开源 实时修复
    Nuwa 大众点评 开源 冷启动修复
    RocooFix 百度金融 开源 冷启动修复
    Aceso 美丽说蘑菇街 开源 实时修复
    Amigo 饿了么 开源 冷启动修复
    Tinker 微信 开源 冷启动修复
    Sophix alibaba 未开源 实时修复 + 冷启动修复

    代码修复(今日主题 - 类加载方式)

    底层替换方式

    • 在已加载的类中直接替换原有方法,是在原有类的基础上进行修改,无法实现对原有类进行方法和字段的增减,这样会破坏原有类的结构。
    • 不稳定。直接修改 JVM 方法实体的具体字段来实现的。Android 是开源的,不同的手机厂商开源对代码进行修改,所以像 Andfix 就会出现在部分机型上的修复失败的现象。

    ClassLoader 类加载方式

    • APP 重新启动后,让 ClassLoader 去加载新的类。

    • class 暂未被加载到系统中,收到推送利用插桩原理让 ClassLoader 优先加载修复好的 dex 。

    实现自己的热修复框架

    Dex 分包

    65536 限制

    com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536
    
    

    当应用程序报 65536 错误的根本原因是,应用的方法数量超过了最大数 65536 个,因为 DVM Bytecode 的限制, DVM 指令集的方法调用指令 invoke-kind 索引为 16 bits, 最多能引用 65535 个方法

    LinearAlloc 限制

    INSTALL_FAILED_DEXOPT
    
    

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

    解决

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

    • gradle 配置

      android {
          compileSdkVersion 26
          defaultConfig {
              applicationId "com.ykun.hotfix"
              minSdkVersion 15
              targetSdkVersion 26
              versionCode 1
              versionName "1.0"
              testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
      
              // 开启分包
              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.keep', // 打包到主classes.dex的文件列表
                                       '--minimal-main-dex'
              ]
          }
      
      }
      
      
    • 配置 multidex.keep 将指定的 class 放入 class.dex 中

      格式:

      //参考
      com/ykun/hotfix/BaseActivity.class
      com/ykun/hotfix/BaseApplication.class
      com/ykun/hotfix/MainActivity.class
      
      
    • 效果

    什么是插桩?

    源码:

    /**遍历需要找到需要加载的 class */ 
    public Class findClass(String name, List<Throwable> suppressed) {
            for (Element element : dexElements) {
                DexFile dex = element.dexFile;
    
                if (dex != null) {
                    Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                    if (clazz != null) {
                        return clazz;
                    }
                }
            }
            if (dexElementsSuppressedExceptions != null) {
                suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
            }
            return null;
        }
    
    

    插桩原理:

    通过源码得知 findClass 是通过遍历 dexElements 来找到 class, 如果我们反射得到 DexPathList 的私有数组 dexElements,我们外部改变这个数组内部顺序索引,将修复好的 dex 放入 [0] 的位置,那么是不是能够优先使用修复好的 dex 勒? 很明显,是成立的。下面开始撸代码吧。

    代码实现

    1. 接收来至服务器发来的补丁包,如果修复包已经存在则删除,copy 到私有目录防止用户不小心删除。

      /**这里模拟已经下载好的 dex 补丁包*/    
      private void downloadPatch() {
              //1 从服务器下载dex文件 比如v1.1修复包文件(classes2.dex)
              File sourceFile = new File(Environment.getExternalStorageDirectory(), "classes2.dex");
              // 目标路径:私有目录
              //getDir("odex", Context.MODE_PRIVATE) data/user/0/包名/app_odex
              File targetFile = new File(getDir("hotfix",
                      Context.MODE_PRIVATE).getAbsolutePath() + File.separator + "classes2.dex");
              if (targetFile.exists()) {
                  targetFile.delete();
              }
              try {
                  // 复制dex到私有目录
                  FileUtils.copyFile(sourceFile, targetFile);
                  Toast.makeText(this, "Bug 修复成功!", Toast.LENGTH_SHORT).show();
                  FixDexUtils.loadFixedDex(this);
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      
      
    2. 创建修复包的类加载器 DexClassLoader (通过源码得知是继承的 BaseDexClassLoader)

          /**
           * 创建类加载器
           *
           * @param context
           * @param fileDir
           */
          private static void createDexClassLoader(Context context, File fileDir) {
              String optimizedDirectory = fileDir.getAbsolutePath() + File.separator + "opt_dex";
              File fOpt = new File(optimizedDirectory);
              if (!fOpt.exists()) {
                  fOpt.mkdirs();
              }
              DexClassLoader classLoader;
              for (File dex : loadedDex) {
                  //初始化类加载器
                  classLoader = new DexClassLoader(dex.getAbsolutePath(), optimizedDirectory, null,
                          context.getClassLoader());
                  //热修复
                  hotFix(classLoader, context);
              }
          }
      
      
    3. 获取系统的 PathClassLoader

      PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
      
      
    4. 获取修复包的 dexElements

      Object pathList = ReflectUtils.reflect(myClassLoader).field("pathList").get();
      Object myDexElements = ReflectUtils.reflect(pathList).field("dexElements").get();
      
      
    5. 获取系统的 dexElements

      Object sysPathList = ReflectUtils.reflect(pathClassLoader).field("pathList").get();
      Object sysDexElements = ReflectUtils.reflect(sysPathList).field("dexElements").get();
      
      
    6. 将系统的 dexElements 和 修复包的 dexElements merge 成新的 dexElements

      // 合并,这里利用插桩原理进行合并数组,将修复好的 class2.dex 放入第一位,优先加入就行了
      Object dexElements = ArrayUtils.combineArray(myDexElements, sysDexElements);
      
      
    7. 重新赋值给 DexPathList 的 dexElements 属性

      //重新赋值
      ReflectUtils.reflect(sysPathList).field("dexElements", dexElements);
      
      

    热修复未来发展

    1. 热修复 = “黑科技”?

      • 热修复不同于国内 APP 进程保活这种 “黑科技”,让 app 常驻后台,既耗电又占用内存,浪费很多手机资源。还有 APP 的推送服务,无节操地对用户进行信息轰炸。还有更无节操的全家桶 app。导致 Android手机卡顿不堪,这些所谓的 “黑科技” 都是为了手机厂商的利益而损害用户的体验。

      • 而热修复是能够让开发者和用户双赢的。不仅厂商能快速迭代更新 app,使功能尽快上线,而且热更新过程用户无感知,节省大量更新时间,提高用户体验。更重要的能保证 app 的功能稳定,bug 能及时修复。

    2. IOS 封杀了热修复功能,Android 的热修复也会被 pass 掉吗?

      • google 和 apple 公司在中国的 diwei 不一样

      • Android 和 IOS 的开放性不同

    3. 热修复未来发展前景是很乐观的。

    热修复进阶学习视频链接https://pan.baidu.com/s/1pXCwbVhxIN873Mk79JJ8BA
    提取码:bxwz

    Android高级系列视频教程

    深度对接腾讯T3高级工程师级别的技术体系,并且综合了目前的各大互联网公司如华为,抖音,OPPO,阿里等主流技术(即使你不想选择腾讯,其它的大厂照样适合)

    腾讯T3级别 Android高级技术视频教程.png
    免费学习可以加入QQ群Android架构师预备营,群内提供高级进阶免费学习课程和视频等资源;964557053 点击链接加入群聊
    注意,以下几类读者请千万不要加入本群:

    1.不愿意潜心学习,幻想只靠短期学习几个星期就可以月薪30K+,就可以进大厂的人。
    2.认为我在打广告骗大家去学习高级进阶技术从而影响转行发展打算的人
    3.不相信努力可以改变命运的人。

    相关文章

      网友评论

        本文标题:性能优化——APP 稳定性之热修复原理探索

        本文链接:https://www.haomeiwen.com/subject/tibmfctx.html