热修复详解与实战#
前言#
随着Google推出Android平台之后,AndroidAPP数量激增,带来的安全类技术需求增长,热修复就是安全类技术的典型产物。2016年起热修复、插件化、动态化技术如雨后春笋般爆发,三者技术上存在一定交集,但热修复更聚焦线上BUG的无感知修复。本文参考了一系列技术文章、书籍、开源框架、商业产品。主要讨论了热修复的以下几点内容:
- 热修复的发展历史
- 热修复的定义
- 热修复的用途
- 热修复的实现思路-分类介绍
- 热修复的实现思路选型
- 热修复的技术方案-分类介绍
- 热修复的技术方案选型
本文适合以下目的的读者:
- 了解热修复概念术语
- 了解热修复的用途
- 了解为新项目集成热修复功能的成本
- 现有热修复技术方案的选型对比
热修复的定义#
正常发版流程是开发者打包4.0版本APK,运营人员将最新APK推送到各大手机应用市场、用户手机通知栏,引导用户下载安装最新版的APK。一般而言用户就能正常体验App的新功能了。一旦出现异常情况,用户在使用新版App的过程中,遇到了bug,如App闪退、App页面显示错误等情况,可能会给用户造成困惑。为了解决用户的困惑,开发者需要紧急修复该bug,并重新走发版流程,引导用户重新下载、安装4.1版本APK。
好消息是一个bug被开发者消灭了;坏消息是用户在漫长的等待过程中,卸载了App,公司最终因为这个bug损失了一批用户。这种负面情况,在按用户流量给企业估值的做法造成毁灭性的打击,这个经济成本是大多数公司无法承担的。
因此,即时、快速、无感的修复线上bug,成为了安全类技术的热门话题。
关于热修复的定义,笔者参考阿里发布的《深入探索Android热修复技术原理》[1]一文:
App直接从云端下拉补丁直接应用生效,重构了传统的App发版流程。
可以看到在遇到bug后,开发人员可以通过修复,并且打出补丁,通知App自动拉取的方式省去了提示用户去应用市场下载,点击通知栏从服务器下载最新APK等“烦人”的做法。让一个Bug对企业的负面影响降到最低。
作为企业技术决策人员,决策一个项目是否应该采取热修复技术,取决于企业是否需要以下几项热修复天然的优势:
- 无需推送应用市场、无需等待应用市场审核
- 无需提示用户下载最新APK
- 实时无感修复
- 用户感知小,线上响应快,企业损失最低
修复Bug、修复bug、修复bug,重要的话说三遍,热修复技术最重要的用途就是无感修复bug。
热修复的实现思路#
思路概览#
用户在操作一款App的过程中,能够体验到的内容如图所示,在不讨论Hybrid技术、Web开发技术的前提下,我们先承认一个简单的事实:安卓操作系统加载了APP安装包下的某些文件,换句话说APK的主要组成部分,支撑了App页面的显示和加载。用户在使用App过程中遇到了bug,是由于Apk中的某些内容遇到了预期之外的情况导致。作为开发者,若想修复这种预期之外的情况,就应该修复Apk中的主要内容来达到目的。
一个APK的组成部分如下
名称 | 用途 |
---|---|
assets | 静态资源目录,如zip资源、网页资源 |
com/google | 特殊包名,存储一些三方依赖的文件如AIDL、cpp源码 |
lib | 子目录armeabi存放的是一些so文件 |
META-INF | 存放的是签名信息;编译生成一个apk包时,会对所有要打包的文件做一个校验计算,并把计算结果放在META-INF目录下 |
res | 静态资源图片、动画、字符串、主题、颜色等 |
resources.arsc | 编译后的二进制资源文件的索引(apk文件的资源表索引) |
AndroidManifest.xml | 它描述了应用的名字、版本、权限、引用的库文件等等信息,用于告诉安卓系统、手机应用市场App的信息 |
classes.dex | java文件经jdk编译成.class文件后,由dex编译成.dex文件 |
用户看到一个bug,通常都是由上述某一部分导致的,因此,我们需要通过技术手段替换某些异常的文件,如资源文件、classes.dex文件、AndroidManifest.xml文件等等。
在得到一个简单的修复方向后,接下来的问题就“容易”许多了:
- 替换谁-找出异常的文件
- 如何替换-下发到用户手机App
笔者结合市面上所有的技术方案,将热修复技术分为三个大类:代码修复、资源修复、SO修复
代码修复#
代码修复涉及到4类技术:NativeHook、Java multidex、Java Hook、Dex替换
NativeHook#
NativeHook参考阅读
NativeHook技术可以简单理解为替换虚拟机中的变量和操作,APK中的文件被安卓系统的虚拟机加载执行,我们可以通过NativeHook技术实时修改虚拟机运行中的变量和操作,从而达到修复Bug的目的。
该方案的缺点是兼容各个安卓版本、各个手机厂商的虚拟机底层实现。
Java multidex#
Java multidex参考阅读
Java multidex技术简单理解是修改了类加载器加载dex文件的顺序,主要原理是
Hook了ClassLoader.pathList.dexElements[]。因为ClassLoader的findClass是通过遍历dexElements[]中的dex来寻找类的。当然为了支持4.x的机型,需要打包的时候进行插桩。
越靠前的Dex优先被系统使用,基于类级别的修复
该方案缺点如下:
- 下次启动才生效
- 消耗性能,即Dalvik平台存在插桩导致的性能损耗,Art平台由于地址偏移问题导致补丁包可能过大的问题
- 虚拟机在安装期间为类打上CLASS_ISPREVERIFIED标志是为了提高性能的,我们强制防止类被打上标志是否会影响性能?但是在大项目中拆分dex的问题已经比较严重,很多类都没有被打上这个标志。
Java Hook#
Java Hook参考阅读
Java Hook技术主要使用了反射和动态代理的Java API,修改类、方法的实现。类A、方法A指向了一个错误的实现类、实现方法等,通过JavaHook改变类A、方法A的指向,指向一个新的正常的类、方法等。
该方案缺点如下:
- 代码是侵入式的,需要在原有的类中加入调整指向相关的代码、调整业务逻辑的代码
- 不支持so和资源的替换
- 显著增大apk体积,1个函数增加17.47个字节,10万个函数会增加1.67M[2]
Dex替换#
Dex替换参考阅读
Dex替换技术顾名思义,替换的是APK目录下的classes.dex文件,替换方式有全量替换和差量替换,全量替换是全量替换APK中的dex文件;差量替换是指仅替换dex文件中不一样的内容。实现该方案难度较大,建议使用Tinker实现代码修复的功能。服务端做dex差量,将差量包下发到客户端,在ART模式的机型上本地跟原apk中的classes.dex做merge,merge成为一个新的merge.dex后将merge.dex插入pathClassLoader的dexElement,原理类同Q-Zone,为了实现差量包的最小化,Tinker自研了DexDiff/DexMerge算法。Tinker还支持资源和So包的更新,So补丁包使用BsDiff来生成,资源补丁包直接使用文件md5对比来生成,针对资源比较大的(默认大于100KB属于大文件)会使用BsDiff来对文件生成差量补丁。
该方案缺点如下:
- 替换的是APK包的dex文件,需要下一次加载APK才能生效
- dex替换操作消耗虚拟机内存,如果遇到OOM有可能合并失败
- 如果App本身可用内存过低,会导致App被系统杀死
综合方案#
Sophix综合了上述的方案优缺点,最终采用从虚拟机层面修改底层的ArtMethod数据结构和排列,实现了代码修复。
该方案的缺点如下:
- 原有类结构发生变化,不适用修复
- 反射调用的非静态方法,不适用修复
资源修复#
笔者最大的疑问:下载全量包、差量包的方式完成替换,与下载APK,并重新安装APK的做法有什么区别?
- APK安装过程是什么?
全量包#
Amigo参考阅读
[Amigo 源码解读 - Eleme Mobilists](http://mobilists.eleme.io/2016/09/01/Amigo 源码解读/)
Amigo支持全量替换和拆分包替换,也支持即时生效和二次启动生效。
立即生效方式:
1 | Amigo.work(context, apkFile); |
稍后生效方式:
1 | Amigo.workLater(context, apkFile, callback); |
该方案缺点如下:
不支持和Instant Run同时使用(或者使用
autoDisableInInstantRunMode
来自动停止使用amigo)patch包中使用provider的方式发生了变化
修改声明方式,authorities须以”${youPackageName}.provider“开头
<provider
android:name="me.ele.demo.provider.StudentProvider" android:authorities="${youPackageName}.provider.student" />
修改调用方式
1
2
3
4// 1. app进程内使用时,无需做任何修改
Cursor cursor = getContentResolver().query(Uri.parse("content://" + getPackageName() + ".provider.student?id=0"), null, null, null, null);
// 2. 其他进程中的使用时,需要修改uri为以下形式, 其中targetPackageName为你的App的包名
Cursor cursor = getContentResolver().query(Uri.parse("content://" + targetPackageName + ".provider/student?id=0"), null, null, null, null);
notification
&widget
中RemoteViews
的自定义布局不支持修改,只支持内容修复- 任何使用在
RemoteViews
里面的资源id都需要进行这样的包装java RCompat.getHostIdentifier(Context context, int id)
- 任何使用在
差量包#
Tinker
sophix
SO修复#
接口实现#
Tinker
插桩实现#
Amigo\sophix
参考#
Dex分包#
动态加载Dex技术
名称 | |
---|---|
Google dex分包工具multidex | |
美团 | |
Androiddex分包方案 | |
cutom class loading in dalvik |
热补丁#
Tinker | QZone | AndFix(Sophix) | Small | Robust | Nuwa | Amigo | |
---|---|---|---|---|---|---|---|
类替换 | yes | yes | no | no | yes | yes | |
So替换 | yes | no | no | no | no | yes | |
资源替换 | yes | yes | no | no | yes | yes | |
全平台支持 | yes | yes | yes | yes | yes | yes | |
即时生效 | no | no | yes | yes | no | 可选(可即时、可冷启动) | |
性能损耗 | 较小 | 较大 | 较小 | 较小 | 较大 | ||
补丁包大小 | 较小 | 较大 | 一般 | 一般 | 较大 | 很大 | |
开发透明 | yes | yes | no | no | yes | yes | |
复杂度 | 较低 | 较低 | 复杂 | 复杂 | 较低 | 较低 | |
gradle支持 | yes | no | no | no | yes | ||
打包方式 | 依赖侵入式打包 | 依赖侵入式打包 | 无侵入式打包 | 依赖侵入式打包 | 依赖侵入式打包 | 依赖侵入式打包 | 依赖侵入式打包 |
Rom体积 | 较大 | 较小 | 较小 | 较小 | 较小 | 较小 | |
成功率 | 较高 | 较高 | 一般 | 最高-99% | 较高 | 最高-100% | |
监控能力 | 提供分发控制及监控 | no | 提供分发控制及监控 | no | no | no | 提供分发控制及控制 |
开发者 | 腾讯团队 | 腾讯团队 | 阿里团队 | 个人 | 美团团队 | 大众点评团队 | 饿了么团队 |
是否开源 | 是,有商业版本 | 否 | 是,有商业版本HotFix和Sophix | 是 | 是 | 是 | 是 |
是否收费 | 收费(基础版免费,但有限制) | 否 | Sophix收费(设有免费阈值5万台) | 否 | 否 | 免费 | 否 |
最近维护时间 | 2021年4月 | 否 | 2016年 | 2018年 | 2020年 | 2015年 | 2017年(被阿里收购不再维护) |
热补丁原理#
热补丁参考文章#
名称 | 内容大纲 |
---|---|
Github Hotfix | Github全栈Hotfix标签项目 |
QQ空间安卓App热补丁动态修复技术介绍 | 1.基于的是android dex分包方案 2.用的是在字节码插入代码,而不是源代码插入 |
Android热修复技术原理详解(最新最全版本) | 1.什么是热修复? 2.热修复框架分类 3.技术原理及特点 4.Tinker框架解析 5.各框架对比图 6.总结 |
阿里《深入探索Android热修复技术原理》 | 阿里出版很详细 |
Amigo源码解读 | 饿了么官方出品 |
Android热更新方案Robust | |
蘑菇街Android热修复探索之路 | 基于 Instant Run Hot Swap |
Instant Run英文原文(需要科学上网) | |
Aceso:美丽说蘑菇街开源的 Android 热修复方案 | |
Android 热修复Nuwa的原理及Gradle插件源码解析 | |
微信Android热补丁实践演进之路 | |
Android热修复技术,你会怎么选? | |
Android热修复技术选型——三大流派解析 (qq.com) | 插件化:一个程序划分为不同的部分,以插件的形式加载到应用中去, 本质上它使用的技术还是热修复技术,只是加入了更多工程实践, 让它支持大规模的代码更新以及资源和SO包的更新。 热修复:当线上应用出现紧急BUG,为了避免重新发版, 并且保证修复的及时性而进行的一项在线推送补丁的修复方案。。 |
Android Patch 方案与持续交付 | 腾旭buggly对tinker的支持,后台管理服务 |
- 1.深入探索Android热修复技术原理 ↩
- 2.Android热更新方案Robust ↩