Android 插件化和热修复知识梳理

时间:2022-09-26 19:57:39

概述

在Android开发中,插件化和热修复的话题越来越多的被大家提及,同时随着技术的迭代,各种框架的发展更新,插件化和热修复的框架似乎已经日趋成熟,许多开发者也把这两项技术运用到实际开发协作和正式的产品当中。因此,我们势必需要了解一下这两门技术。

插件化和热修复

首先需要明确的一点,插件化和热修复不是同一个概念,虽然站在技术实现的角度来说,他们都是从系统加载器的角度出发,无论是采用hook方式,亦或是代理方式或者是其他底层实现,都是通过“欺骗”Android 系统的方式来让宿主正常的加载和运行插件(补丁)中的内容;但是二者的出发点是不同的。插件化顾名思义,更多是想把需要实现的模块或功能当做一个独立的提取出来,减少宿主的规模,当需要使用到相应的功能时再去加载相应的模块。热修复则往往是从修复bug的角度出发,强调的是在不需要二次安装应用的前提下修复已知的bug。

为了方便叙述,做以下称谓约定:

宿主: 就是当前运行的APP
插件: 相对于插件化技术来说,就是要加载运行的apk类文件
补丁: 相对于热修复技术来说,就是要加载运行的.patch,.dex,*.apk等一系列包含dex修复内容的文件。

以下提到内容中的宿主和插件(补丁),均是上述含义,不再赘述。

 
Android 插件化和热修复知识梳理
Android插件化技术的典型应用

上图就是对Android插件化和热修复之间关系的体现。据我所知,在某些开发团队中,会把热修复的技术,作为在APP端部署日常活动的功能来用。虽然,实际效果来看是没有问题的,但长期使用还是值得商榷的。

早期很多应用的动态换肤功能,就是参考了Android 插件化的技术,最早的新浪微博夜间模式就是通过下载一个夜间模式的apk文件完成,当时做为开发者的自己,感觉很高级。关于动态加载的应用,其实有很多可以扩展的思路,比如特定节日的促销活动,逃避审核机制的动态广告加载都是Android插件化技术可以考虑的实现,更多内容可以参考Android动态加载技术 简单易懂的介绍方式

下面就从插件化技术的发展源头,逐步叙述一下二者的发展历程及现状,了解一下时至今日,热修复框架的发展到了各种地步,总体梳理一下热修复的原理,对现有的框架有一个了解。

插件化

发展历程及现状

关于插件化技术的起源可以追溯到5年前

  • 2012年的 AndroidDynamicLoader ,他的原理是动态加载不同的Fragment实现UI替换,可以说是开山鼻祖了,但是这种方案可扩展性不强。

  • 再到后来出现了23Code,他可以直接下载一个自定义控件的demo,并且运行起来。

  • 2014年一个里程碑式的年份,任玉刚(俗称主席)发布了dynamic-load-apk,也叫做DL。在这个框架里提供了两个很重要的思路:

    • 如何管理插件内Activity的生命周期: 使用 DLProxyActivity 采用静态代理的方式去调用插件中Activity的生命周期方法。
    • 如何加载插件内的资源文件:通过反射调用AssetManager 中到的addAssetPath方法就可以将特定路径的资源加载到系统内存中使用。

    以上两点,可以说是非常有意义的,尤其是第二点关于插件资源的记载,是后期出现的许多框架的参考思路。这个框架也有一些局限,不支持插件内Service、BroadcastReceiver等需要注册才能使用的组件,同时插件apk也需要按照其开发规范来实现,总体来说还是有一定的成本,但无论怎样都是一个很有价值的框架。(话说这个框架貌似已经不再维护了,最近一次关于代码的更新都是2年前了,o(╥﹏╥)o)。

  • 2015年 DroidPlugin
    DroidPlugin 是Andy Zhang在Android系统上实现了一种新的 插件机制 :它可以在无需安装、修改的情况下运行APK文件,此机制对改进大型APP的架构,实现多团队协作开发具有一定的好处。 这段话是DroidPlugin在Github README 文档中的介绍。这款来自360的插件化框架.

  • 2015年 DynamicAPK 这个就……,貌似因为License的原因已经完全不更新了。

  • 2017 RePlugin 这是360 开源的插件化框架,按照他自己的说法,相较于其他框架,他对系统的hook只有一处,那就是ClassLoader,这样从理论来说,貌似会有更好的稳定性。

  • 2017年 atlas这个是阿里今年刚刚开源的插件化开发框架,可以说是非常强大;具体原理参考详解 Atlas 框架原理;还没有用过。

  • Small 最后再说一下Small,个人感觉Small 所提供了一种比插件化更高层次的概念,组件化;把一个完整的APP看成是由许多可以复用模块组件组成(这个有点像React Native的开发理念);开发起来像是搭积木的感觉。有兴趣的可以去Small官网了解一下。

热修复

相较于插件化,热修复技术的使用更加的频繁,因为这项技术切实关切到我们实际开发的产品,能够更快速更便捷的修复线上bug,才能带来更好的用户体验。因此下面就结合热修复的原理了解一下热修复的使用及发展现状。

以下所有分析源自热修复相关文章,这里只是把结论整理了出来。具体分析就不再拾人牙慧了,对实现细节有兴趣的同学可以查看相应的链接。

类加载原理

说起热修复就不得不提类的加载机制,和常规的JVM类似,在Android中类的加载也是通过ClassLoader来完成,具体来说就是PathClassLoader 和 DexClassLoader 这两个Android专用的类加载器,这两个类的区别如下:

  • PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
  • DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,也就是我们一开始提到的补丁。

这两个类都是继承自BaseDexClassLoader,我们可以看一下BaseDexClassLoader的构造函数。

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

这个构造函数只做了一件事,就是通过传递进来的相关参数,初始化了一个DexPathList对象。DexPathList的构造函数,就是将参数中传递进来的程序文件(就是补丁文件)封装成Element对象,并将这些对象添加到一个Element的数组集合dexElements中去

ClassLoaer 的加载机制是一种特别聪明的方式,双亲委托机制,在这种机制下,一个Class只会被加载一次。

对于ClassLoader加载机制及双亲委托机制的分析可以参考Android解析ClassLoader(一)Java中的ClassLoader

这里需要明白的一点是对于一个ClassLoader(类加载器)来说,将一个具体的类(class)加载到内存中其实是由虚拟机完成的,对于开发者来说,我们关注的重点应该是如何去找到这个需要加载的类

假设我们现在要去查找一个名为name的class,那么DexClassLoader将通过以下步骤实现:

  • 在DexClassLoader的findClass 方法中通过一个DexPathList对象findClass()方法来获取class
  • 在DexPathList的findClass 方法中,对之前构造好dexElements数组集合进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。

总的来说,通过DexClassLoader查找一个类,最终就是就是在一个数组中查找特定值的操作。

综合以上所有的观点,我们很容易想到一种非常简单粗暴的热修复方案。假设现在代码中的某一个类或者是某几个类有bug,那么我们可以在修复完bug之后,可以将这些个类打包成一个补丁文件,然后通过这个补丁文件封装出一个Element对象,并且将这个Element对象插到原有dexElements数组的最前端,这样当DexClassLoader去加载类时,优先会从我们插入的这个Element中找到相应的类,虽然那个有bug的类还存在于数组中后面的Element中,但由于双亲加载机制的特点,这个有bug的类已经没有机会被加载了,这样一个bug就在没有重新安装应用的情况下修复了。

有了上面的思路,其实我们就可以自己动手去实现一个简单的热修复框架了。这里推荐一篇
热修复——深入浅出原理与实现,文中作者深入分析了热修复原理,并基于以上原理实现了一个基础的热修复框架,实现过程分析的非常细致深入,非常适合做为热修复入门原理的了解。

QQ 空间超级补丁方案

看完上面的原理,是不是觉得热修复很简单,没什么可研究的呢?其实不然,Java是一门面向对象的语言,我们使用的类会有继承关系,会相互依赖引用。同时Android虚拟机和常规的JVM 不同,加载的并不是.class而是dex(准确的来说是经过优化的odex),在这样一个过程中,势必会有一些新的问题值得我们去关注。这个问题就是的CLASS_ISPREVERIFIED,什么意思呢。

  • 在apk安装的时候系统会将dex文件优化成odex文件,在优化的过程中会涉及一个预校验的过程
  • 如果一个类的static方法,private方法,override方法以及构造函数中引用了其他类,而且这些类都属于同一个dex文件,此时该类就会被打上CLASS_ISPREVERIFIED
  • 如果在运行时被打上CLASS_ISPREVERIFIED的类引用了其他dex的类,就会报错
  • 正常的分包方案会保证相关类被打入同一个dex文件
  • 想要使得patch可以被正常加载,就必须保证类不会被打上CLASS_ISPREVERIFIED标记。而要实现这个目的就必须要在分完包后的class中植入对其他dex文件中类的引用

以上内容摘自Android热修复技术——QQ空间补丁方案解析(2)

要在已经编译完成后的类中植入对其他类的引用,就需要操作字节码,惯用的方案是插桩。常见的工具有javaassist,asm等

QQ 空间补丁方案就是使用javaassist 插桩的方式解决了CLASS_ISPREVERIFIED的难题。

Tinker

QQ空间超级补丁,“超级补丁”很多情况下意味着补丁文件很大,而将这样一个大文件夹加载在内存中构建一个Element对象,插入到数组最前端是需要耗费时间的,无疑会印象应用启动的速度。因此Tinker 提出了另外一种思路。

 
Android 插件化和热修复知识梳理
image

图片源自https://github.com/Tencent/tinker

Tinker的思路是这样的,通过修复好的class.dex 和原有的class.dex比较差生差量包补丁文件patch.dex,在手机上这个patch.dex又会和原有的class.dex 合并生成新的文件fix_class.dex,用这个新的fix_class.dex 整体替换原有的dexPathList的中的内容,可以说是从根本上把bug给干掉了。

Tinker 提供的思路可以说是非常新奇,也非常值得我们去学习。上图中过程看似简单,但其实具体实现起来还真的不简单。你有想过两个.dex 是如何比较得出差异化文件patch.dex 的吗?有兴趣的同学可以看看鸿翔的这篇分析Android 热修复 Tinker 源码分析之DexDiff / DexPatch

当然,需要注意的是,patch.dex和原先的class.dex 合并的时候需要新的进程去完成,同时考虑的现在大多数应用的规模,multidex已经是很常见的事情了,因此多个dex 之间的合并策略及成功率,都是在使用Tinker时需要考虑的问题。

关于Tinker 更多细节可以参考 微信Android热补丁实践演进之路

Tinker 提供的文档及example非常完善,对于有兴趣接入的开发者可以说是非常友好了,但总体来说接入过程还是有些复杂,对整个项目的侵入还是较强,Tinker是个人唯一使用过的热修复的框架,总体来说还是不错的,通过接入到实际应用中,对gradle也有了新的认识,对gradle有兴趣的同学,其实可以看看tinker的gradle接入方式

HotFix

以上提到的两种方式,虽然策略有所不同,但总的来说都是从上层ClassLoader的角度出发,由于ClassLoader的特点,如果想要新的补丁文件再次生效,无论你是插桩还是提前合并,都需要重新启动应用来加载新的DexPathList。这样就无法在用户神不知鬼不觉的情况下把bug修复了,HotFix在这方面就有绝对的优势了。

HotFix(即AndFix),是在AndFix 的基础之上提供了补丁安全服务及版本管理等相关内容,方便广大的开发人员使用。

AndFix 提供了一种运行时在Native修改Filed指针的方式,实现方法的替换,达到即时生效无需重启,对应用无性能消耗的目的。

 
Android 插件化和热修复知识梳理
AndFix 原理

更多细可以参考https://github.com/alibaba/AndFix,Native层不怎么理解,就不强行装逼了o(╯□╰)o。

由于他是Native层操作,因此如果我们在Java层中新增字段,或者是修改类的方法,他是无能为力的。同时由于Android在国内变成了安卓,各大手机厂商定制了自己的ROM,所以很多底层实现的差异,导致AndFix的兼容性并不是很好。

Sophix

阿里推出业界首个非侵入式热修复方案Sophix,颠覆移动端传统发版更新流程!

这是我第一次了解到Sophix时看到的文章标题原文链接;对于技术类的文章来说,敢于使用颠覆这两个字,要么是标题党;要么就是真的很有货。

Sophix 可以说是博采众长,前面提到的Tinker及AndFix 都在某一方面存在缺陷。因此Sophix 便取长补短,采用全量替换的思路,从一种更高的层次实现了热修复。这貌似也是事物发展的一贯规律,后来的新生事物总结前人的经验教训,吸收好的思想,变得更好。

关于Sophix 的原理看了很多篇文章,感觉这篇干货满满,Android热修复方案介绍分析的不错,有兴趣的可以看一下。

总的来说,Sophix应该是现有最成熟的热修复方案了。

其他及总结

当然就热修复的实现,各个大厂还有各自的实现,比如饿了吗的Amigo,美团的Robust,实现及优缺点各有差异,但总的来说就是两大类

  • ClassLoader 加载方案
  • Native层替换方案

或者是参考Android Studio Instant Run 的思路实现代码整体的增量更新。但这样势必会带来性能的影响。

综上所述,其实对于热修复很难有一种十分完美的解决方案。在Android开发中,四大组件使用前需要在AndroidManifest中提前声明,而如果需要使用热修复的方式,无论是提前占坑亦或是动态修改,都会带来很强的侵入性(因此,Sophix是不支持四大组件修复的,这也是其非侵入性设计理念无法避免的事情了,不知道以后会不会有新的办法)。再者Android碎片化的问题,对热修复方案的适配也是一个考验。通过查看几大以开源在Github上的热修复方案,在issue中可以看到提到最多的问题还是兼容性。

因此,面对实际的开发,选择使用或者说选择哪种方案,必须符合实际的应用的场景,一句话,没有最好的,只有合适的。


好了,插件化和热修复知识就梳理到这里了