tinker

微信热修复框架Tinker集成记录

Tinker 是微信官方的热补丁的解决方案,它支持动态下发代码 So库以及资源,让应用能够在不需要重新安装的情况下实现更新- Tinker
head_img

Tinker和其他热修复的区别

功能 Tinker Qzone AndFix Robust
类替换 yes yes no no
So替换 yes no no no
资源替换 yes yes no no
全平台支持 yes yes yes yes
即时生效 no no yes yes
性能损耗 较小 较大 较小 较小
补丁包大小 较小 较大 一般 一般
复杂度 较低 较低 复杂 复杂
开发透明 yes yes no no
Rom体积 较大 较小 较小 较小
成功率 较高 较高 一般 最高
gradle支持 yes no no no

在project的build.gradle中添加依赖

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

在app的build.gradle中配置一些设置

1
2
3
4
compile "com.android.support:appcompat-v7:23.1.1"
compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }

其中tinker_version在project的gradle.properties里设置

1
TINKER_VERSION=1.7.6

app/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
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
def gitSha() {
try {
// String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
String gitRev = "1008611"
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'")
}
}
def javaVersion = JavaVersion.VERSION_1_7
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
compileOptions {
sourceCompatibility javaVersion
targetCompatibility javaVersion
}
//recommend
dexOptions {
jumboMode = true
}
signingConfigs {
release {
try {
storeFile file("./keystore/release.keystore")//签名设置
storePassword "testres"
keyAlias "testres"
keyPassword "testres"
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
debug {
storeFile file("./keystore/debug.keystore")
}
}
defaultConfig {
applicationId "com.example.liangmutian.mskyswiperefreshlayout"
minSdkVersion 10
targetSdkVersion 22
versionCode 1
versionName "1.0.0"
/**
* you can use multiDex and install it in your ApplicationLifeCycle implement
*/
multiDexEnabled true
/**
* buildConfig can change during patch!
* we can use the newly value when patch
*/
buildConfigField "String", "MESSAGE", "\"I am the base apk\""
// buildConfigField "String", "MESSAGE", "\"I am the patch apk\""
/**
* client version would update with patch
* so we can get the newly git version easily!
*/
buildConfigField "String", "TINKER_ID", "\"${getTinkerIdValue()}\""
buildConfigField "String", "PLATFORM", "\"all\""//拼出一些信息
}
// aaptOptions{
// cruncherEnabled false
// }
// //use to test flavors support //多渠道打包的设置
// productFlavors {
// flavor1 {
// applicationId 'tinker.sample.android.flavor1'
// }
//
// flavor2 {
// applicationId 'tinker.sample.android.flavor2'
// }
// }
buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
debuggable true
minifyEnabled false
signingConfig signingConfigs.debug
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
def bakPath = file("${buildDir}/bakApk/")
/**
* you can use assembleRelease to build you base apk
* use tinkerPatchRelease -POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE= to build patch
* add apk from the build/bakApk
*/
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
//for normal build
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/app-debug-58-33.apk"//选择打包出来的名字 数字要对上
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-debug-29-33-mapping.txt" //混淆之后的mapping文件
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-debug-58-33-R.txt"//混淆之后的id对应
//only use for build all flavor, if not, just ignore this field
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 {
/**
* necessary,default 'null'
* the old apk path, use to diff with the new apk to build
* add apk from the build/bakApk
*/
oldApk = getOldApkPath()
/**
* optional,default 'false'
* there are some cases we may get some warnings
* if ignoreWarning is true, we would just assert the patch process
* case 1: minSdkVersion is below 14, but you are using dexMode with raw.
* it must be crash when load.
* case 2: newly added Android Component in AndroidManifest.xml,
* it must be crash when load.
* case 3: loader classes in dex.loader{} are not keep in the main dex,
* it must be let tinker not work.
* case 4: loader classes in dex.loader{} changes,
* loader classes is ues to load patch dex. it is useless to change them.
* it won't crash, but these changes can't effect. you may ignore it
* case 5: resources.arsc has changed, but we don't use applyResourceMapping to build
*/
ignoreWarning = false
/**
* optional,default 'true'
* whether sign the patch file
* if not, you must do yourself. otherwise it can't check success during the patch loading
* we will use the sign config with your build type
*/
useSign = true
/**
* optional,default 'true'
* whether use tinker to build
*/
tinkerEnable = buildWithTinker()
/**
* Warning, applyMapping will affect the normal android build!
*/
buildConfig {
/**
* optional,default 'null'
* if we use tinkerPatch to build the patch apk, you'd better to apply the old
* apk mapping file if minifyEnabled is enable!
* Warning:
* you must be careful that it will affect the normal assemble build!
*/
applyMapping = getApplyMappingPath()
/**
* optional,default 'null'
* It is nice to keep the resource id from R.txt file to reduce java changes
*/
applyResourceMapping = getApplyResourceMappingPath()
/**
* necessary,default 'null'
* because we don't want to check the base apk with md5 in the runtime(it is slow)
* tinkerId is use to identify the unique base apk when the patch is tried to apply.
* we can use git rev, svn rev or simply versionCode.
* we will gen the tinkerId in your manifest automatic
*/
tinkerId = getTinkerIdValue()
/**
* if keepDexApply is true, class in which dex refer to the old apk.
* open this can reduce the dex diff file size.
*/
keepDexApply = false
}
dex {
/**
* optional,default 'jar'
* only can be 'raw' or 'jar'. for raw, we would keep its original format
* for jar, we would repack dexes with zip format.
* if you want to support below 14, you must use jar
* or you want to save rom or check quicker, you can use raw mode also
*/
dexMode = "jar"
/**
* necessary,default '[]'
* what dexes in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
*/
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
/**
* necessary,default '[]'
* Warning, it is very very important, loader classes can't change with patch.
* thus, they will be removed from patch dexes.
* you must put the following class into main dex.
* Simply, you should add your own application {@code tinker.sample.android.SampleApplication}
* own tinkerLoader, and the classes you use in them
*
*/
loader = [
//use sample, let BaseBuildInfo unchangeable with tinker
"tinker.sample.android.app.BaseBuildInfo"
]
}
lib {
/**
* optional,default '[]'
* what library in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
* for library in assets, we would just recover them in the patch directory
* you can get them in TinkerLoadResult with Tinker
*/
pattern = ["lib/armeabi/*.so"]
}
res {
/**
* optional,default '[]'
* what resource in apk are expected to deal with tinkerPatch
* it support * or ? pattern.
* you must include all your resources in apk here,
* otherwise, they won't repack in the new apk resources.
*/
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
/**
* optional,default '[]'
* the resource file exclude patterns, ignore add, delete or modify resource change
* it support * or ? pattern.
* Warning, we can only use for files no relative with resources.arsc
*/
ignoreChange = ["assets/sample_meta.txt"]
/**
* default 100kb
* for modify resource, if it is larger than 'largeModSize'
* we would like to use bsdiff algorithm to reduce patch file size
*/
largeModSize = 100
}
packageConfig {
/**
* optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
* package meta file gen. path is assets/package_meta.txt in patch file
* you can use securityCheck.getPackageProperties() in your ownPackageCheck method
* or TinkerLoadResult.getPackageConfigByName
* we will get the TINKER_ID from the old apk manifest for you automatic,
* other config files (such as patchMessage below)is not necessary
*/
configField("patchMessage", "tinker is sample to use")
/**
* just a sample case, you can use such as sdkVersion, brand, channel...
* you can parse it in the SamplePatchListener.
* Then you can use patch conditional!
*/
configField("platform", "all")
/**
* patch version via packageConfig
*/
configField("patchVersion", "1.0")
}
//or you can add config filed outside, or get meta value from old apk
//project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
//project.tinkerPatch.packageConfig.configField("test2", "sample")
/**
* if you don't use zipArtifact or path, we just use 7za to try
*/
sevenZip {
/**
* optional,default '7za'
* the 7zip artifact path, it will use the right 7za with your platform
*/
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
/**
* optional,default '7za'
* you can specify the 7za path yourself, it will overwrite the zipArtifact value
*/
// path = "/usr/local/bin/7za"
}
}
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each {flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
//def date = new Date().format("MMdd-HH-mm-ss")
def date = new Date().format("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"
}
}
}
}
}

然后把 sample里的类导入

这是图片啊

修改sampleappactivitylike的上方的注释 这注释的意思就是 在指定包下生成一个amsky的aoolication类

这是图片啊

在manifest里读取内存的权限

1
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
1
2
3
4
5
6
7
<application
android:name=".AMSKY” 设置application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
1
2
3
<service 加入处理加载补丁的服务
android:name=".service.SampleResultService"
android:exported="false"/>

执行api更新代码

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
public boolean showInfo(Context context) {
// add more Build Info
final StringBuilder sb = new StringBuilder();
Tinker tinker = Tinker.with(getApplicationContext());
if (tinker.isTinkerLoaded()) {
sb.append(String.format("[220000220009999990000patch is loaded] \n"));
sb.append(String.format("[buildConfig TINKER_ID] %s \n", BuildInfo.TINKER_ID));
sb.append(String.format("[buildConfig BASE_TINKER_ID] %s \n", BaseBuildInfo.BASE_TINKER_ID));
sb.append(String.format("[buildConfig MESSSAGE] %s \n", BuildInfo.MESSAGE));
sb.append(String.format("[TINKER_ID] %s \n", tinker.getTinkerLoadResultIfPresent().getPackageConfigByName(ShareConstants.TINKER_ID)));
sb.append(String.format("[packageConfig patchMessage] %s \n", tinker.getTinkerLoadResultIfPresent().getPackageConfigByName("patchMessage")));
sb.append(String.format("[TINKER_ID Rom Space] %d k \n", tinker.getTinkerRomSpace()));
} else {
sb.append(String.format("[2200002200099999990000patch is not loaded] \n"));
sb.append(String.format("[buildConfig TINKER_ID] %s \n", BuildInfo.TINKER_ID));
sb.append(String.format("[buildConfig BASE_TINKER_ID] %s \n", BaseBuildInfo.BASE_TINKER_ID));
sb.append(String.format("[buildConfig MESSSAGE] %s \n", BuildInfo.MESSAGE));
sb.append(String.format("[TINKER_ID] %s \n", ShareTinkerInternals.getManifestTinkerID(getApplicationContext())));
}
sb.append(String.format("[BaseBuildInfo Message] %s \n", BaseBuildInfo.TEST_MESSAGE));
final TextView v = new TextView(context);
v.setText(sb);
v.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
v.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 10);
v.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
v.setTextColor(0xFF000000);
v.setTypeface(Typeface.MONOSPACE);
final int padding = 16;
v.setPadding(padding, padding, padding, padding);
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setCancelable(true);
builder.setView(v);
final AlertDialog alert = builder.create();
alert.show();
return true;
}
private void doReF() {
swipeRefreshLayout.setRefreshing(true);
listener.onRefresh();
}
public void re(View v) {
doReF();
}
public void load(View v) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), "/sdcard/zhibo8/patch_signed_7zip.apk");
}
public void show(View v) {
showInfo(MainActivity.this);
}
public void clean(View v) {
Tinker.with(getApplicationContext()).cleanPatch();
}
public void kill(View v) {
ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
android.os.Process.killProcess(android.os.Process.myPid());
}
@Override
protected void onResume() {
Log.e(TAG, "i am on onResume");
super.onResume();
Utils.setBackground(false);
}
@Override
protected void onPause() {
Log.e(TAG, "i am on onPause");
super.onPause();
Utils.setBackground(true);
}
}

先安装打出来的有问题的包

这是图片啊

这是图片啊

这是图片啊

这是图片啊

然后把这个patch_signed_7zip.apk放进手机 然后在更新代码 就会发现我们修改的代码已经生效了

TinkerPatch

TinkerPatch,是微信微Tinker推出的平台,来帮助开发者更好的管理补丁的下发和管理(腾讯的Bugly也可以,也是使用tinker方案)

平台 配置基本和自己集成差不多 为了编写方便观看 把对tinker的设置抽了出来

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
apply plugin: 'tinkerpatch-support'
tinkerpatchSupport {
def bakPath = file("${buildDir}/bakApk/")
tinkerEnable = true
appKey = "你的appkey" //在Tinker平台申请的appkey
/** 在 TinkerPatch 平台 输入的版本号, 例如 sample 中的 '1.0.0'。
注意,我们使用 appVersion 作为 TinkerId, 我们需要保证每个发布出去的基础安装包的 appVersion 都不一样。只是补丁的版本
注意: 若发布新的全量包, appVersion一定要更新 **/
appVersion = "1.0"
reflectApplication = false
autoBackupApkPath = "${bakPath}"
baseApkFile = "${bakPath}/app-1.0-0111-17-21-53/app-debug.apk"
baseProguardMappingFile = "${bakPath}/app-1.0-0111-17-21-53/app-debug-mapping.txt"
baseResourceRFile = "${bakPath}/app-1.0-0111-17-21-53/app-debug-R.txt"
}
tinkerPatch {
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
}
}

并在app的builde.gradle 中引入

1
2
apply from: 'tinkerpacth.gradle'

application有两种设置

这是图片啊

reflectApplication = true 的情况

若我们使用 reflectApplication 模式,我们无需为接入 Tinker 而改造我们的 Application 类。初始化 SDK 可参考 tinkerpatch-easy-sample 中的 SampleApplication 类.

1
2
3
4
5
6
public class SampleApplication extends Application {
...
public void attachBaseContext(Context base) {
TinkerPatch.init(TinkerPatchApplicationLike.getTinkerPatchApplicationLike());
}

我们将 Tinker 加载补丁过程的结果存放在 TinkerPatchApplicationLike 中。

  1. reflectApplication = false 的情况

若我们已经完成了应用的 Application 改造,即将 Application 的逻辑移动到 ApplicationLike类中。我们可以参考 tinkerpatch-sample 中的 SampleApplicationLike 类. 修改注释的方法和自己集成一样,并在mainfeast里注册application

1
2
3
4
5
6
7
public class SampleApplicationLike extends DefaultApplicationLike {
...
public void onBaseContextAttached(Context base) {
TinkerPatch.init(this);
}
}

还需要配置轮训服务器的一些设置

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
public class FetchPatchHandler extends Handler {
public static final long HOUR_INTERVAL = 3600 * 1000;
private long checkInterval;
/**
* 通过handler, 达到按照时间间隔轮训的效果
* @param hour
*/
public void fetchPatchWithInterval(int hour) {
//设置TinkerPatch的时间间隔
TinkerPatch.with().setFetchPatchIntervalByHours(hour);
checkInterval = hour * HOUR_INTERVAL;
//立刻尝试去访问,检查是否有更新
sendEmptyMessage(0);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//这里使用false即可
TinkerPatch.with().fetchPatchUpdate(false);
//每隔一段时间都去访问后台, 增加10分钟的buffer时间
sendEmptyMessageDelayed(0, checkInterval + 10 * 60 * 1000);
}
}

发布时候appversion版本号每一个都要不一样发布的时候版本号和gridle里的appversion保持一致
我们app本来的appversion就是我们升级的时候需要修改的和这个不一样,可以自己设定一个规则,比如一个三位和我们app本身的versioncode保持一致,然后每一个版本可以重复发布升级补丁。

API 详情见tinkerpatch的文档

版本问题
经过反复测试
多次升级补丁之后,你安装的版本还是当初你用apk安装的版本无论你中间安装了多少个升级包,都是当初安装的那个版本

这个补丁是 一直对照基础包来打差异包

就是1升2的时候 (2-1)+1

2升3 (3-1)+1

3升4 (4-1)+1

多渠道

关于渠道包的问题,若使用 flavor 编译渠道包,会导致不同的渠道包由于 BuildConfig 变化导致 classes.dex 差异。这里建议的方式有:

将渠道信息写在 AndroidManifest.xml 或文件中,例如 channel.ini;

将渠道信息写在 apk 文件的 zip comment 中,这种是建议方式,例如可以使用项目 packer-ng-plugin 或者可使用 V2 Scheme 的 walle;

若不同渠道存在功能上的差异,建议将差异部分放于单独的dex或采用相同代码不同配置方式实现;

已通过Walle实现:

【Android】Walle多渠道打包&Tinker热修复
http://www.jianshu.com/p/0ba717f7385f

Tinker已知问题

Tinker 不支持修改 AndroidManifest.xml,Tinker 不支持新增四大组件;

由于 Google Play 的开发者条款限制,不建议在GP渠道动态更新代码;

在 Android N 上,补丁对应用启动时间有轻微的影响;

不支持部分三星 android-21 机型,加载补丁时会主动抛出 TinkerRuntimeException:checkDexInstall failed;

由于各个厂商的加固实现并不一致,在1.7.6以及之后的版本,tinker 不再支持加固的动态更新;

对于资源替换,不支持修改 remoteView。例如 transition动画,notification icon 以及 桌面图标。