Android热修复-微信Tinker

写在前面

正常情况下一旦线上版本出BUG时,这时候得改BUG,重新发布上线,用户重新下载安装,成本未免有点高;基于这种情况下很多热修复框架孕育而生,比较火的有:Andfix、HotFix等;本文旨在帮助没接触过Tinker的童鞋快速集成使用热修复;

本文环境

官方地址
SdkVersion 24
gradle:2.2.0
Tinker版本 1.7.5

集成Tinker

在项目根目录的build.gradle中添加,例:

     
     
1
2
3
4
     
     
dependencies {
···
classpath( 'com.tencent.tinker:tinker-patch-gradle-plugin:1.7.5')
}

切换到module的build.gradle中添加,例:

     
     
1
2
3
4
5
     
     
dependencies {
···
provided( 'com.tencent.tinker:tinker-android-anno:1.7.5')
compile( 'com.tencent.tinker:tinker-android-lib:1.7.5')
}

这里我把打包签名的jks文件配置到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
     
     
android {
···
signingConfigs {
release {
try {
storeFile file("./keystore/app.jks")
storePassword "password"
keyAlias "alias"
keyPassword "password"
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
debug {
try {
storeFile file("./keystore/app.jks")
storePassword "password"
keyAlias "alias"
keyPassword "password"
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
}
}

OK,到这里配置还是相对简单的,下面配置Tinker的gradle插件,官方demo配置还是比较麻烦的,看着就头晕,建议可以把先前的内容先复制出来,在拷贝官方demo的build.gradle进去一点点修改;

如果还是懒得改,直接把下面内容复制到build.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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
     
     
/**-----------------------------------配置开始-----------------------------------*/
def bakPath = file( "${buildDir}/bakApk/")
def gitSha() {
try {
String gitRev = '11.2.3.5'
if (gitRev == null) {
throw new GradleException( "can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
return gitRev
} catch (Exception e) {
throw new GradleException( "can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
}
ext {
tinkerEnabled = true
//旧apk
tinkerOldApkPath = "${bakPath}/app-debug-0118-15-13-26.apk"
//旧包混淆文件
tinkerApplyMappingPath = "${bakPath}/app-debug-1018-17-32-47-mapping.txt"
//旧apk R文件
tinkerApplyResourcePath = "${bakPath}/app-debug-0118-15-13-26-R.txt"
//多渠道
tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}
def getOldApkPath() {
return hasProperty( "OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}
def getApplyMappingPath() {
return hasProperty( "APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}
def getApplyResourceMappingPath() {
return hasProperty( "APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}
def getTinkerIdValue() {
return hasProperty( "TINKER_ID") ? TINKER_ID : gitSha()
}
def buildWithTinker() {
return hasProperty( "TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}
if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'
tinkerPatch {
/**
* 基准apk包路径,也就是旧包路径
* */
oldApk = getOldApkPath()
/**
* 如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。因为这些情况可能会导致编译出来的patch包
* 带来风险:
* 1. minSdkVersion小于14,但是dexMode的值为"raw";
* 2. 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
* 3. 定义在dex.loader用于加载补丁的类不在main dex中;
* 4. 定义在dex.loader用于加载补丁的类出现修改;
* 5. resources.arsc改变,但没有使用applyResourceMapping编译。
* */
ignoreWarning = false
/**
* 在运行过程中,我们需要验证基准apk包与补丁包的签名是否一致,是否需要为你签名
* */
useSign = true
buildConfig {
/**
* 可选参数;在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。这个只
* 是推荐的,但设置applyMapping会影响任何的assemble编译。
* */
applyMapping = getApplyMappingPath()
/**
* 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的
* 大小,同时也避免由于ResId改变导致remote view异常。
* */
applyResourceMapping = getApplyResourceMappingPath()
/**
* 在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些
* 基准包上面,一般来说我们可以使用git版本号、versionName等等。
* */
tinkerId = getTinkerIdValue()
}
dex {
/**
* 只能是'raw'或者'jar'。
* 对于'raw'模式,我们将会保持输入dex的格式。
* 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式
* ,而且它更省存储空间,但是验证md5时比'raw'模式耗时()
* */
dexMode = "jar"
/**
* 是否提前生成dex,而非合成的方式。这套方案即回退成Qzone的方案,对于需要使用加固或者多flavor打包(建
* 议使用其他方式生成渠道包)的用户可使用。但是这套方案需要插桩,会造成Dalvik下性能损耗以及Art补丁包可
* 能过大的问题,务必谨慎使用。另外一方面,这种方案在Android N之后可能会产生问题,建议过滤N之后的用户。
*/
usePreGeneratedPatchDex = false
/**
* 需要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如/assets/...
*/
pattern = [ "classes*.dex",
"assets/secondary-dex-?.jar"]
/**
* 这一项非常重要,它定义了哪些类在加载补丁包的时候会用到。这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。
这里需要定义的类有:
1. 你自己定义的Application类;
2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;
3. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
4. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。这里需要注意的是,这些类的直接引用类也
需要加入到loader中。或者你需要将这个类变成非preverify。
*/
loader = [ "com.tencent.tinker.loader.*",
//warning, you must change it with your application
//TODO 换成自己的Application
"com.tinker.MyApplication",
]
}
lib {
/**
* 需要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如/assets/...
*/
pattern = [ "lib/armeabi/*.so"]
}
res {
/**
* 需要处理res路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如/assets/...,
* 务必注意的是,只有满足pattern的资源才会放到合成后的资源包。
*/
pattern = [ "res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
/**
* 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。
* 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。
*/
ignoreChange = [ "assets/sample_meta.txt"]
/**
* 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,但是会增加合成
* 时的复杂度。默认大小为100kb
*/
largeModSize = 100
}
packageConfig {
/**
* configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动
* 写入configField。在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。但是建议直接通过修改代码来实现,例如BuildConfig。
*/
configField( "patchMessage", "tinker is sample to use")
}
sevenZip {
/**
* 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用
*/
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
}
/**
* 文件名 描述
* patch_unsigned.apk 没有签名的补丁包
* patch_signed.apk 签名后的补丁包
* patch_signed_7zip.apk 签名后并使用7zip压缩的补丁包,也是我们通常使用的补丁包。但正式发布的时候,最好不要以.apk结尾,防止被运营商挟持。
* log.txt 在编译补丁包过程的控制台日志
* dex_log.txt 在编译补丁包过程关于dex的日志
* so_log.txt 在编译补丁包过程关于lib的日志
* tinker_result 最终在补丁包的内容,包括diff的dex、lib以及assets下面的meta文件
* resources_out.zip 最终在手机上合成的全量资源apk,你可以在这里查看是否有文件遗漏
* resources_out_7z.zip 根据7zip最终在手机上合成的全量资源apk
* tempPatchedDexes 在Dalvik与Art平台,最终在手机上合成的完整Dex,我们可以在这里查看dex合成的产物。
*
*
* */
/**
* 获得所有渠道集合,并判断数量
*/
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
/**
* bak apk and mapping
* 创建Task并执行文件操作
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
def date = new Date().format( "MMdd-HH-mm-ss")
tasks.all {
if ( "assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {
it.doLast {
copy {
def fileNamePrefix = "${project.name}-${variant.baseName}"
def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"
def destPath = hasFlavors ? file( "${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
from variant.outputs.outputFile
into destPath
rename { String fileName ->
fileName.replace( "${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
}
from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
into destPath
rename { String fileName ->
fileName.replace( "mapping.txt", "${newFileNamePrefix}-mapping.txt")
}
from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
into destPath
rename { String fileName ->
fileName.replace( "R.txt", "${newFileNamePrefix}-R.txt")
}
}
}
}
}
}
/**
* 如果有渠道则进行多渠道打包
*/
project.afterEvaluate {
//sample use for build all flavor for one time
if (hasFlavors) {
task(tinkerPatchAllFlavorRelease) {
group = 'tinker'
def originOldPath = getTinkerBuildFlavorDirectory()
for (String flavor : flavors) {
def tinkerTask = tasks.getByName( "tinkerPatch${flavor.capitalize()}Release")
dependsOn tinkerTask
def preAssembleTask = tasks.getByName( "process${flavor.capitalize()}ReleaseManifest")
preAssembleTask.doFirst {
String flavorName = preAssembleTask.name.substring( 7, 8).toLowerCase() + preAssembleTask.name.substring( 8, preAssembleTask.name.length() - 15)
project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"
}
}
}
task(tinkerPatchAllFlavorDebug) {
group = 'tinker'
def originOldPath = getTinkerBuildFlavorDirectory()
for (String flavor : flavors) {
def tinkerTask = tasks.getByName( "tinkerPatch${flavor.capitalize()}Debug")
dependsOn tinkerTask
def preAssembleTask = tasks.getByName( "process${flavor.capitalize()}DebugManifest")
preAssembleTask.doFirst {
String flavorName = preAssembleTask.name.substring( 7, 8).toLowerCase() + preAssembleTask.name.substring( 8, preAssembleTask.name.length() - 13)
project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
}
}
}
}
}
}
}
/**-----------------------------------配置结束-----------------------------------*/

注意tinkerId目前暂时是写死的;
新建SampleApplicarion文件继承DefaultApplicationLike,例:

     
     
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
     
     
@DefaultLifeCycle(application = "com.tinker.MyApplication",flags = ShareConstants.TINKER_ENABLE_ALL)
public class SampleApplicarion extends DefaultApplicationLike {
public SampleApplicarion(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent, Resources[] resources, ClassLoader[] classLoader, AssetManager[] assetManager) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent, resources, classLoader, assetManager);
}
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
TinkerInstaller.install( this);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}
}

上面的application=”你的包名.MyApplication”并在上一步将dex中的loader修改为:

     
     
1
     
     
loader = [ "com.tencent.tinker.loader.*", "你的包名.MyApplication",]

记得在AndroidManifest.xml中添加:

     
     
1
2
3
4
5
6
7
8
9
10
11
12
     
     
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<application
android:name= ".MyApplication"
android:allowBackup= "true"
android:icon= "@mipmap/ic_launcher"
android:label= "@string/app_name"
android:supportsRtl= "true"
android:theme= "@style/AppTheme">
···
···
</application>

OK,到这里就算配置完成了,配置完了当然要测试了;

测试修复BUG

在MainActivity中添加完初始代码后点击Build->Build APK生成APK;
描述
将outputs里面的apk拷贝出去安装在手机上,然后打开的build.radle修改ext,其它不管,例:

     
     
1
2
3
4
5
6
7
8
9
     
     
ext {
tinkerEnabled = true
//旧apk
tinkerOldApkPath = "${bakPath}/app-debug-0118-16-38-14.apk"
···
//旧apk R文件
tinkerApplyResourcePath = "${bakPath}/app-debug-0118-16-38-14-R.txt"
···
}

然后切换到MainActivity修复你的bug,这里我添加了一张资源图片(刚好同事在玩支付宝集福,就P了张有9张”敬业福”的图片骗他们,哈哈),修复好代码后点击右侧Gradle找到:app->Tasks->tinker,然后双击tinkerPatchDebug后会在outputs下面生成tinkerPatch文件夹,其中的patch_signed_7zip.apk就是你需要的补丁:
描述
将patch_signed_7zip.apk文件放到你设置的SD卡目录,调用Tinker加载补丁,例:

     
     
1
     
     
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), patchPath);

如果加载成功下面的tinker.isTinkerLoaded()返回的是true,例:

     
     
1
2
3
4
5
6
7
     
     
Tinker tinker = Tinker.with(getApplicationContext());
boolean isLoadSuccess = tinker.isTinkerLoaded();
if(isLoadSuccess){
Log.i(TAG, "success");
} else{
Log.i(TAG, "error");
}

OK,加载补丁前点击”SHOW”会抛出空指针异常,修复完后点击”SHOW”,BUG解决:

DEMO地址

总结

Tinker是腾讯出品,微信在用,我想兼容性应该不会差到哪去,相比其他热修复框架,这是个很大的优点,而且Tinker支持类替换、so库及资源的替换等,如果在相对稳定的情况下,使用Tinker用于线上产品的功能升级应该没什么问题,其它更高级的用法自行深入研究;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值