项目场景:
- 紧急发现了一个bug,影响用户体验,阻断项目流程。这个时候,只能紧急发布一个强制更新的新版本,让用户升级。
- 最近百团大战开始。需要增加一个活动弹窗入口,越快越好。这个时候,只能紧急发布一个强制更新的新版本,让用户升级。
存在需求:
1 | 可不可以不让用户重新安装就可以解决上述场景? |
什么是热更新:
1 | 让应用能够在无需重新安装的情况实现更新,帮助应用快速建立动态修复能力。 |
从上面的定义来看,热补丁节省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 文件中添加插件依赖。
1
2
3
4
5
6
7
8
9buildscript {
repositories {
jcenter()
}
dependencies {
// tinkersupport插件,其中latest.release指代最新版本号,也可以指定明确的版本号,例如1.0.8
classpath "com.tencent.bugly:tinker-support:latest.release"
}
}在app module 下的build.gradle 文件中添加 配置
1
2
3
4
5
6dependencies {
compile "com.android.support:multidex:1.0.1"
compile 'com.tencent.bugly:crashreport_upgrade:latest.release'
}
// 依赖插件脚本
apply from: 'tinker-support.gradle'在同级目录下创建 tinker-support.gradle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90apply 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配置。
1
2
3
4
5
6
7
8
9
10
11
12<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" />混淆配置
1 | -dontwarn com.tencent.bugly.** |
本文转载自:https://www.dazhuanlan.com/2019/12/31/5e0b56c7943ed/
乱码三千 – 点滴积累 ,欢迎来到乱码三千技术博客站