项目场景:
- 紧急发现了一个bug,影响用户体验,阻断项目流程。这个时候,只能紧急发布一个强制更新的新版本,让用户升级。
- 最近百团大战开始。需要增加一个活动弹窗入口,越快越好。这个时候,只能紧急发布一个强制更新的新版本,让用户升级。
存在需求:
可不可以不让用户重新安装就可以解决上述场景?
什么是热更新:
让应用能够在无需重新安装的情况实现更新,帮助应用快速建立动态修复能力。
从上面的定义来看,热补丁节省Android大量应用市场发布的时间。同时用户也无需重新安装,只要上线就能无感知的更新。
热更新的原理:
现在市面上主流的几大热更新技术:
- 淘宝 Dexposed
- 支付宝 AndFix
- Qzone 超级热补丁
- 微信 Tinker
Dexposed
基于 Xposed 实现的无侵入的运行时 AOP (Aspect-oriented Programming) 框架,可以实现在线修复 Bug,修复粒度方法级别,这也就意味着我们没有办法进行类的增减操作。而且由于对 ART 虚拟机不支持,导致其对 Android 5.0、6.0 均不支持,使用局限性太大。
AndFix
native hook 方式,其核心部分在 JNI 层对方法进行替换,替换有问题的方法,修复粒度方法级别,无法在类中新增和删减字段,可以做到即时生效。也就是运行时生效。但是因为它的核心部分在JNI,所以会出现很多适配兼容的问题。因为国内的rom厂商多才多艺.
超级热补丁
使用新的 ClassLoader 加载 patch.dex,hack 默认的 ClassLoader,替换有问题的类,修复粒度类级别,一般无法做到即时生效,需要在应用下一次启动时生效。但是在art虚拟机中,如果改变了类变量,和方法名,有可能导致内存错乱的问题,没有开源这个项目。但在github上的Nuwa采用了相同的方式,这个是开源。
Tinker
dex 文件全量替换,基于 DexDiff 技术,对比修复前后的 dex 文件,生成 patch.dex,再根据 patch.dex 更新有问题的 dex 文件。简单来说,在编译时通过新旧两个Dex生成差异patch.dex。在运行时,将差异patch.dex重新跟原始安装包的旧Dex还原为新的Dex。这个过程可能比较耗费时间与内存,所以我们是单独放在一个后台进程:patch中。为了补丁包尽量的小,微信自研了DexDiff算法,它深度利用Dex的格式来减少差异的大小。
热更新方案的比较:
Tables | Tinker | Qzone | Andfix | Dexposed |
---|---|---|---|---|
类替换 | yes | yes | no | no |
lib替换 | yes | no | no | no |
资源替换 | yes | yes | no | no |
全平台支持 | yes | yes | yes | no |
即时生效 | no | no | yes | yes |
性能损耗 | 较小 | 较大 | 较小 | 较小 |
补丁包大小 | 较小 | 较大 | 一般 | 一般 |
开发透明 | yes | yes | no | no |
复杂度 | 较低 | 较低 | 复杂 | 复杂 |
gradle支持 | yes | yes | no | no |
接口文档 | 丰富 | 一般 | 一般 | 较少 |
占rom体积 | 较大 | 较小 | 较小 | 较小 |
成功率 | 较好 | 最高 | 一般 | 一般 |
热更新的使用场景:
热补丁技术也可以理解为一个动态修改代码与资源的通道,它适合于修改量较少的情况。
我们看一下微信的版本升级的情况:
Tables | 普通升级 | 布丁升级 |
---|---|---|
数据大小 | 33M | 145K |
更新速度 | 10天 | 1天(70%) |
自动升级 | wifi | 移动网络 |
以Android用户的升级习惯,即使是相对活跃的微信也需要10天以上的时间去覆盖50%的用户。使用补丁技术,我们能做到1天覆盖70%以上。这也是基于补丁体积较小,可以直接使用移动网络下载更新。
热更新使用限制
- 补丁只能针对单一客户端版本,随着版本差异变大补丁体积也会增大;
- 补丁不能支持所有的修改,例如AndroidManifest;
- 补丁无论对代码还是资源的更新成功率都无法达到100%。
如何在一个项目中增加热更新功能?
在工程目录 build.gradle 文件中添加插件依赖。
buildscript { repositories { jcenter() } dependencies { // tinkersupport插件,其中latest.release指代最新版本号,也可以指定明确的版本号,例如1.0.8 classpath "com.tencent.bugly:tinker-support:latest.release" } }
在app module 下的build.gradle 文件中添加 配置
dependencies { compile "com.android.support:multidex:1.0.1" compile 'com.tencent.bugly:crashreport_upgrade:latest.release' } // 依赖插件脚本 apply from: 'tinker-support.gradle'
在同级目录下创建 tinker-support.gradle
apply plugin: 'com.tencent.bugly.tinker-support' def bakPath = file("${buildDir}/bakApk/") /** * 此处填写每次构建生成的基准包目录 */ def baseApkDir = "app-0912-17-04-44" /** * 对于插件各参数的详细解析请参考 */ tinkerSupport { // 开启tinker-support插件,默认值true enable = true //自动生成tinkerId,无须关注此。默认为false //autoGenerateTinkerId = true tinkerEnable = true // 指定归档目录,默认值当前module的子目录tinker autoBackupApkDir = "${bakPath}" // 是否启用覆盖tinkerPatch配置功能,默认值false // 开启后tinkerPatch配置不生效,即无需添加tinkerPatch overrideTinkerPatchConfiguration = true // 编译补丁包时,必需指定基线版本的apk,默认值为空 // 如果为空,则表示不是进行补丁包的编译 // @{link tinkerPatch.oldApk } baseApk = "${bakPath}/${baseApkDir}/com.nongfenqi.sherlock-release-v2.3.2_32.apk" // 对应tinker插件applyMapping baseApkProguardMapping = "${bakPath}/${baseApkDir}/app-release-mapping.txt" // 对应tinker插件applyResourceMapping baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-release-R.txt" // 构建基准包和补丁包都要指定不同的tinkerId,并且必须保证唯一性 tinkerId = "2.3.2-0912-patch" // 构建多渠道补丁时使用 // buildAllFlavorsDir = "${bakPath}/${baseApkDir}" // 是否启用加固模式,默认为false.(tinker-spport 1.0.7起支持) // isProtectedApp = true // 是否开启反射Application模式 enableProxyApplication = false } /** * 一般来说,我们无需对下面的参数做任何的修改 * 对于各参数的详细介绍请参考: * https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97 */ tinkerPatch { //oldApk ="${bakPath}/${appName}/app-release.apk" tinkerEnable = true ignoreWarning = false useSign = true dex { dexMode = "jar" pattern = ["classes*.dex"] loader = [] } lib { pattern = ["lib/*/*.so"] } res { pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] ignoreChange = [] largeModSize = 100 } packageConfig { } sevenZip { zipArtifact = "com.tencent.mm:SevenZip:1.1.10" // path = "/usr/local/bin/7za" } buildConfig { keepDexApply = false //tinkerId = "1.0.1-base" //applyMapping = "${bakPath}/${appName}/app-release-mapping.txt" // 可选,设置mapping文件,建议保持旧apk的proguard混淆方式 //applyResourceMapping = "${bakPath}/${appName}/app-release-R.txt" // 可选,设置R.txt文件,通过旧apk文件保持ResId的分配 } }
权限配置以及activity配置。
<uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.READ_LOGS" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <activity android:name="com.tencent.bugly.beta.ui.BetaActivity" android:theme="@android:style/Theme.Translucent" />
混淆配置
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}