Android Tinker集成采坑

Android Tinker集成采坑

官方文档 https://github.com/Tencent/tinker/wiki

官方demo怎么配置都可以从demo中找到 https://github.com/Tencent/tinker/tree/dev/tinker-sample-android

Tinker提供了两种接入方式,命令行接入和gradle接入。正常的项目中都基本都使用gradle,一次配置好以后就可以很方便的使用了,所以本次只使用gradle方式。

本文基于1.9.13版本,因为有好几个地方都需要用到版本信息,所以将它放在gradle.properties文件中方便版本的管理

1
TINKER_VERSION=1.9.13

在总工程的的build.gradle配置tinker的classpath,因为tinker定义了一些自己的gradle脚本,后面在配置参数的时候会用到。

1
2
3
4
classpath("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}") {
changing = TINKER_VERSION?.endsWith("-SNAPSHOT")
exclude group: 'com.android.tools.build', module: 'gradle'
}

然后在app的gradle文件中配置核心库和谷歌的分包库,现在的应用功能都很多所以体积很大一般都会用到multidex

1
2
3
4
5
6
7
8
9
//核心sdk库
api("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
implementation("com.tencent.tinker:tinker-android-loader:${TINKER_VERSION}") { changing = true }

//注解编译器,生成application的时候用
annotationProcessor("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
compileOnly("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }

implementation "com.android.support:multidex:1.0.3"

先配置app的gradle文件中android这个标签下的内容

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
 //配置签名,这里使用demo中的签名文件,真实项目中替换成自己的
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")
}
}
// 支持大工程模式
dexOptions {
jumboMode = true
}
//release包开始混淆
buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), project.file('proguard-rules.pro')
}
debug {
debuggable true
minifyEnabled false
signingConfig signingConfigs.debug
}
}

然后开始配置tinker的参数,官方指南上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
def bakPath = file("${buildDir}/bakApk/")

ext {
//是否启用tinker
tinkerEnabled = true
//每次打包完都需要更改下面的三个路径,如果支持多渠道打包,下面第四个参数也需要修改
//old apk 的路径
tinkerOldApkPath = "${bakPath}/app-release-0508-10-52-50.apk"
//old apk 混淆 mapping 文件的路径
tinkerApplyMappingPath = "${bakPath}/app-release-0508-10-52-50-mapping.txt"
//old apk R文件的路径
tinkerApplyResourcePath = "${bakPath}/app-release-0508-10-52-50-R.txt"

//多渠道打包的路径
tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}

static def gitSha() {
// 每次打包的时候版本要一致,官方demo的是git的版本,这里使用versionName
String gitRev = "1.0"
return gitRev
}

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") ? Boolean.parseBoolean(TINKER_ENABLE) : ext.tinkerEnabled
}

def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}
//判断是否启用tinker
if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'

tinkerPatch {
/**
* old apk 的路径
*/
oldApk = getOldApkPath()
/**
* 在产生patch的时候是否忽略tinker的警告,最好不忽略
* case 1: minSdkVersion小于14,但是dexMode的值为"raw"
* case 2: 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
* case 3: 定义在dex.loader用于加载补丁的类不在main dex中;
* case 4: 定义在dex.loader用于加载补丁的类出现修改;
* case 5: resources.arsc改变,但没有使用applyResourceMapping编译
*/
ignoreWarning = false

/**
* 是否启用签名,一般强制使用
*/
useSign = true

/**
* 是否启用tinker
*/
tinkerEnable = buildWithTinker()

/**
* Warning, applyMapping will affect the normal android build!
*/
buildConfig {
/**
* 指定old apk 混淆时的打包文件
*/
applyMapping = getApplyMappingPath()
/**
* 指定old apk 的资源文件
*/
applyResourceMapping = getApplyResourceMappingPath()

/**
* 每个patch文件的唯一标识符
*/
tinkerId = getTinkerIdValue()

/**
* 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
*/
keepDexApply = false

/**
* 是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。
*/
isProtectedApp = false

/**
* 是否支持新增非export的Activity
*/
supportHotplugComponent = false
}

dex {
/**
* 只能是'raw'或者'jar'。
* 对于'raw'模式,我们将会保持输入dex的格式。
* 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式,
* 而且它更省存储空间,但是验证md5时比'raw'模式耗时。默认我们并不会去校验md5,一般情况下选择jar模式即可。
*/
dexMode = "jar"

/**
* 需要处理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。
* 5. 使用1.7.6版本之后的gradle版本,参数1、2会自动填写。若使用newApk或者命令行版本编译,1、2依然需要手动填写
*/
loader = [
//use sample, let BaseBuildInfo unchangeable with tinker
// "com.hsm.tinkertest.BuildInfo"
]
}
//lib相关的配置项
lib {
/**
* 需要处理lib路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,例如assets/...
*/
pattern = ["lib/*/*.so"]
}
//res相关的配置项
res {
/**
* 需要处理res路径,支持*、?通配符,必须使用'/'分割。与dex.pattern一致, 路径是相对安装包的,
* 例如assets/...,务必注意的是,只有满足pattern的资源才会放到合成后的资源包。
*/
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]

/**
* 若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改
*/
ignoreChange = ["assets/sample_meta.txt"]

/**
* 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,
* 但是会增加合成时的复杂度。默认大小为100kb
*/
largeModSize = 100
}
//用于生成补丁包中的'package_meta.txt'文件
packageConfig {
/**
* configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。在这里,
* 你可以定义其他的信息, 在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。
* 但是建议直接通过修改代码来实现,例如BuildConfig。
*/
configField("patchMessage", "tinker is sample to use")
configField("platform", "all")
/**
* patch version via packageConfig
*/
configField("patchVersion", "1.0")
}

/**
* 7zip路径配置项,执行前提是useSign为true
*/
sevenZip {
/**
* 将自动根据机器属性获得对应的7za运行文件
*/
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
def date = new Date().format("MMdd-HH-mm-ss")

/**
* old apk复制到指定目录
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name

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.first().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"
}

}
}
}
}
}

task sortPublicTxt() {
doLast {
File originalFile = project.file("public.txt")
File sortedFile = project.file("public_sort.txt")
List<String> sortedLines = new ArrayList<>()
originalFile.eachLine {
sortedLines.add(it)
}
Collections.sort(sortedLines)
sortedFile.delete()
sortedLines.each {
sortedFile.append("${it}\n")
}
}
}

OK,参数配置完成,下面开始写代码。

先写一个TinkerManager类来管理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
public class TinkerManager {

private static final String TAG = "Tinker.TinkerManager";

private static ApplicationLike applicationLike;
/**
* 保证只初始化一次
*/
private static boolean isInstalled = false;

public static void setTinkerApplicationLike(ApplicationLike appLike) {
applicationLike = appLike;
}

public static ApplicationLike getTinkerApplicationLike() {
return applicationLike;
}


public static void setUpgradeRetryEnable(boolean enable) {
UpgradePatchRetry.getInstance(applicationLike.getApplication()).setRetryEnable(enable);
}

public static void installTinker(ApplicationLike appLike) {
if (isInstalled) {
TinkerLog.w(TAG, "install tinker, but has installed, ignore");
return;
}
//监听patch文件加载过程中的事件
LoadReporter loadReporter = new DefaultLoadReporter(appLike.getApplication());
//监听patch文件合成过程中的事件
PatchReporter patchReporter = new DefaultPatchReporter(appLike.getApplication());
//监听patch文件接收到之后可以做一些校验
PatchListener patchListener = new CustomPatchListener(appLike.getApplication());
//升级策略
AbstractPatch upgradePatchProcessor = new UpgradePatch();

TinkerInstaller.install(appLike,
loadReporter, patchReporter, patchListener,
CustomResultService.class, upgradePatchProcessor);

isInstalled = true;
}

}

这里面有几个类需要注意

  1. LoadReporter类:监听patch文件加载过程中的事件,这里使用DefaultLoadReporter,如果有需要可以继承DefaultLoadReporter写自己的业务逻辑
  2. PatchReporter :监听patch文件合成过程中的事件,这里使用DefaultPatchReporter,如果哟需要可以继承DefaultPatchReporter写自己的业务逻辑
  3. PatchListener :监听patch文件接收到之后可以做一些校验,这个一般用的比较多,为了保证我们下载的patch包的没有被篡改,可以重写PatchListener,写一些自己的校验
  4. AbstractPatch :升级策略,一般不用修改
  5. CustomResultService:继承自系统的DefaultTinkerResultService,决定在patch安装完以后的后续操作,因为tinker修复完之后需要重启才能生效,tinker默认是加载完patch包之后直接杀死进程。这样可能会不太友好,如果不想直接杀进程可以继承DefaultTinkerResultService类,写我们自己的逻辑。

CustomPatchListener和CustomResultService的样例:

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
public class CustomPatchListener extends DefaultPatchListener {

private String currentMD5;

public void setCurrentMD5(String md5Value) {
this.currentMD5 = md5Value;
}
public CustomPatchListener(Context context) {
super(context);
}

/**
* 校验
* @return
*/
@Override
public int patchCheck(String path, String patchMd5) {
//做自己的校验

return super.patchCheck(path, patchMd5);
}
}
/**
* 决定在patch安装完以后的后续操作,默认实现是杀进程
*/
public class CustomResultService extends DefaultTinkerResultService {
private static final String TAG = "Tinker.CustomResultService";

//返回patch文件的结果
@Override
public void onPatchResult(final PatchResult result) {
if (result == null) {
TinkerLog.e(TAG, "CustomResultService received null result!!!!");
return;
}
TinkerLog.i(TAG, "CustomResultService receive result: %s", result.toString());

//first, we want to kill the recover process
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());

Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
if (result.isSuccess) {
Toast.makeText(getApplicationContext(), "patch success, please restart process", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getApplicationContext(), "patch fail, please check reason", Toast.LENGTH_LONG).show();
}
}
});
// is success and newPatch, it is nice to delete the raw file, and restart at once
// for old patch, you can't delete the patch file
if (result.isSuccess) {
deleteRawPatchFile(new File(result.rawPatchFilePath));

//默认是直接重启体验可能不好,这里只是在后台重启
if (checkIfNeedKill(result)) {
if (Utils.isBackground()) {
TinkerLog.i(TAG, "it is in background, just restart process");
restartProcess();
} else {
TinkerLog.i(TAG, "tinker wait screen to restart process");
new Utils.ScreenState(getApplicationContext(), new Utils.ScreenState.IOnScreenOff() {
@Override
public void onScreenOff() {
restartProcess();
}
});
}
} else {
TinkerLog.i(TAG, "I have already install the newly patch version!");
}
}
}

/**
* you can restart your process through service or broadcast
*/
private void restartProcess() {
TinkerLog.i(TAG, "app is background now, i can kill quietly");
//you can send service or broadcast intent to restart your process
android.os.Process.killProcess(android.os.Process.myPid());
}

}

为了使真正的Application实现可以在补丁包中修改,tinker建议Appliction类的所有逻辑移动到ApplicationLike代理类中。

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
@DefaultLifeCycle(application = ".SampleTinkerApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class CustomTinkerLike extends DefaultApplicationLike {
CustomTinkerLike mCustomTinkerLike;
public CustomTinkerLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag,
long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}

@Override
public void onCreate() {
super.onCreate();
}

@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
//必须使用multiDex
MultiDex.install(base);

mCustomTinkerLike = this;
TinkerManager.setTinkerApplicationLike(this);

//在 installed 之前设置
TinkerManager.setUpgradeRetryEnable(true);

//installTinker after load multiDex
//or you can put com.tencent.tinker.** to main dex
TinkerManager.installTinker(this);
Tinker tinker = Tinker.with(getApplication());
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}
}
  • 自定义一个CustomTinkerLike继承自DefaultApplicationLike,以前在我们自定义的Application中初始化的代码都移动到这里的onCreate()方法中。
  • 添加注解DefaultLifeCycle,第一个是application的名字,编译的时候会自动给我们生成一个application类,然后把这个生成的application注册到AndroidManifest.xml中。

最后Activity中定义一个按钮点击加载patch包

1
2
3
4
public void load(View view) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
Environment.getExternalStorageDirectory().getAbsolutePath() + "/patch_signed_7zip.apk");
}

到这里配置和代码就都完成了,下面开始打包,先打基础包

线上的包基本都是release包,前面已经配置了签名,所以这就只打release包。

可以使用命令行输入命令./gradlew assemableRelease,也可以使用studio的快捷操作,快捷操作图片如下

打完包之后,tinker会将outputs/release文件夹下的打包好的文件复制一份到bakApk文件夹中一份,并重命名,这个bakApk文件夹是前面在gradle中配置的。还有混淆的mapping文件和R文件也复制一份重命名放到bakApk文件夹下面。

把打包好的apk装到手机上,然后修改一些代码,开始打补丁包

如图修改gradle中的oldApk的信息。然后调用tinker的命令打包如下图

打包完成之后在outputs文件夹下会多出来一个tinkerPatch文件夹。patch_signed_7zip.apk就死我们需要的patch包了。直接放到前面加载sdk文件的路径,或者从网络下载到该路径,之后调用加载的方法就完成修复了。

# 进阶

コメント

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×