Android实战——Tinker的集成和使用

前言

对于热修复我相信很多小伙伴都已经知道它们普遍的操作套路,Tinker主要是依赖自己的gradlePlugin生成拆分包,所以其拆分包的生成就由Gradle来完成,当然也可以通过命令行的方式,这里就不对命令行做讲解,Tinker接入指南

项目结构

这里写图片描述

Tinker介绍

来自Tinker官方

1、优点

这里写图片描述

2、缺点

  1. Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件(1.9.0支持新增非export的Activity);
  2. 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
  3. 在Android N上,补丁对应用启动时间有轻微的影响;
  4. 不支持部分三星android-21机型,加载补丁时会主动抛出”TinkerRuntimeException:checkDexInstall failed”;
  5. 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。

Tinker集成

1、在项目的build.gradle中,添加依赖

1
2
3
4
5
6
7
8
9
10
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
// Tinker
classpath ("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}")
}
}

这里的TINKER_VERSION写在项目gradle.properties中

1
TINKER_VERSION=1.7.7

2、在app的build.gradle文件,添加依赖

1
2
3
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}")
compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}")
compile "com.android.support:multidex:1.0.1"

添加依赖以后,我们在gradle文件中做以下配置

  1. 开启Multidex、配置Java编译的版本
  2. 配置签名文件,为了后面打包方便调试
  3. 引入另一个gradle文件专门来对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
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.3"
defaultConfig {
applicationId "com.handsome.thinker"
minSdkVersion 16
targetSdkVersion 25
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
multiDexEnabled true
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
dexOptions {
jumboMode = true
}
signingConfigs {
debug {
keyAlias 'hensen'
keyPassword '123456'
storeFile file("../Hensen.jks")
storePassword '123456'
}
release {
keyAlias 'hensen'
keyPassword '123456'
storeFile file("../Hensen.jks")
storePassword '123456'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
debug {
minifyEnabled false
signingConfig signingConfigs.debug
}
}
}
// 加入Tinker生成补丁包的gradle
apply from: 'buildTinker.gradle'

3、buildTinker.gradle是专门为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
//指定生成apk文件的存放位置
def bakPath = file("${buildDir}/bakApk/")
//参数配置
ext {
//开启Tinker
tinkerEnable = true
//旧的apk位置,需要我们手动指定
tinkerOldApkPath = "${bakPath}/"
//旧的混淆映射位置,如果开启了混淆,则需要我们手动指定
tinkerApplyMappingPath = "${bakPath}/"
//旧的resource位置,需要我们手动指定
tinkerApplyResourcePath = "${bakPath}/"
tinkerID = "1.0"
}
def buildWithTinker() {
return ext.tinkerEnable
}
def getOldApkPath() {
return ext.tinkerOldApkPath
}
def getApplyMappingPath() {
return ext.tinkerApplyMappingPath
}
def getApplyResourceMappingPath(){
return ext.tinkerApplyResourcePath
}
def getTinkerIdValue(){
return ext.tinkerID
}
if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'
tinkerPatch {
oldApk = getOldApkPath() //指定old apk文件路径
ignoreWarning = false //不忽略tinker警告,出现警告则中止patch文件生成
useSign = true //patch文件必须是签名后的
tinkerEnable = buildWithTinker() //指定是否启用tinker
buildConfig {
applyMapping = getApplyMappingPath() //指定old apk打包时所使用的混淆文件
applyResourceMapping = getApplyResourceMappingPath() //指定old apk的资源文件
tinkerId = getTinkerIdValue() //指定TinkerID
keepDexApply = false
}
dex {
dexMode = "jar" //jar、raw
pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] //指定dex文件目录
loader = ["com.handsome.thinker.AppLike.MyTinkerApplication"] //指定加载patch文件时用到的类
}
lib {
pattern = ["libs/*/*.so"] //指定so文件目录
}
res {
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] //指定资源文件目录
ignoreChange = ["assets/sample_meta.txt"] //指定不受影响的资源路径
largeModSize = 100 //资源修改大小默认值
}
packageConfig {
configField("patchMessage", "fix the 1.0 version's bugs")
configField("patchVersion", "1.0")
}
}
/**
* 是否配置了多渠道
*/
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
/**
* 复制apk包和其它必须文件到指定目录
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
def date = new Date().format("yyyy-MM-dd-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")
}
}
}
}
}
}
}

4、记得开启Manifest权限,否则生成拆分包的时候有奇怪错误

1
2
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

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
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
public class TinkerManager {
private static boolean isInstalled = false;
// 这里的ApplicationLike可以理解为Application的载体
private static ApplicationLike mAppLike;
private static CustomPatchListener mPatchListener;
/**
* 默认初始化Tinker
*
* @param applicationLike
*/
public static void installTinker(ApplicationLike applicationLike) {
mAppLike = applicationLike;
if (isInstalled) {
return;
}
TinkerInstaller.install(mAppLike);
isInstalled = true;
}
/**
* 初始化Tinker,带有自定义模块
* <p>
* 1、CustomPatchListener
* 2、CustomResultService
*
* @param applicationLike
* @param md5Value 服务器下发的md5
*/
public static void installTinker(ApplicationLike applicationLike, String md5Value) {
mAppLike = applicationLike;
if (isInstalled) {
return;
}
mPatchListener = new CustomPatchListener(getApplicationContext());
mPatchListener.setCurrentMD5(md5Value);
// Load补丁包时候的监听
LoadReporter loadReporter = new DefaultLoadReporter(getApplicationContext());
// 补丁包加载时候的监听
PatchReporter patchReporter = new DefaultPatchReporter(getApplicationContext());
AbstractPatch upgradePatchProcessor = new UpgradePatch();
TinkerInstaller.install(applicationLike,
loadReporter,
patchReporter,
mPatchListener,
CustomResultService.class,
upgradePatchProcessor);
isInstalled = true;
}
/**
* 增加补丁包
*
* @param path
*/
public static void addPatch(String path) {
if (Tinker.isTinkerInstalled()) {
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path);
}
}
/**
* 获取上下文
*
* @return
*/
private static Context getApplicationContext() {
if (mAppLike != null) {
return mAppLike.getApplication().getApplicationContext();
}
return null;
}
}

由于Tinker默认Patch检查是没有对文件做Md5校验,我们可以重写其检验的方法,加上我们自己的检验逻辑(需要自定义模块的方式初始化Tinker)

CustomPatchListener.java

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
public class CustomPatchListener extends DefaultPatchListener {
private String currentMD5;
public void setCurrentMD5(String md5Value) {
this.currentMD5 = md5Value;
}
public CustomPatchListener(Context context) {
super(context);
}
/**
* patch的检测
*
* @param path
* @return
*/
@Override
protected int patchCheck(String path) {
//MD5校验的工具可以网上查找
//这里要求我们在初始化Tinker的时候加上MD5的参数
//增加patch文件的md5较验
if (!MD5Utils.isFileMD5Matched(path, currentMD5)) {
return ShareConstants.ERROR_PATCH_DISABLE;
}
return super.patchCheck(path);
}
}

由于Tinker默认安装完补丁包之后是删除补丁包,然后杀掉进程的方式,我们可以修改杀掉进程的行为(需要自定义模块的方式初始化Tinker)

CustomResultService.java

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
public class CustomResultService extends DefaultTinkerResultService {
private static final String TAG = "Tinker.SampleResultService";
/**
* patch文件的最终安装结果,默认是安装完成后杀掉自己进程
* 此段代码主要是复制DefaultTinkerResultService的代码逻辑
*/
@Override
public void onPatchResult(PatchResult result) {
if (result == null) {
TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!");
return;
}
TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString());
//first, we want to kill the recover process
TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext());
// if success and newPatch, it is nice to delete the raw file, and restart at once
// only main process can load an upgrade patch!
if (result.isSuccess) {
//删除patch包
deleteRawPatchFile(new File(result.rawPatchFilePath));
//杀掉自己进程,如果不需要则可以注释,在这里做自己的逻辑
if (checkIfNeedKill(result)) {
android.os.Process.killProcess(android.os.Process.myPid());
} else {
TinkerLog.i(TAG, "I have already install the newly patch version!");
}
}
}
}

Tinker使用

1、Tinker的使用需要ApplicationLike来生成我们的Application,然后初始化Multidex和Tinker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@DefaultLifeCycle(application = ".MyTinkerApplication", flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false)
public class CustomTinkerLike extends ApplicationLike {
public CustomTinkerLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
MultiDex.install(base);
TinkerManager.installTinker(this);
}
}

2、编译项目自动生成Application,然后在Manifest中指定我们的生成的Application

1
2
<application
android:name=".AppLike.MyTinkerApplication"

3、在主页面按钮的点击事件,来加载放在缓存目录下的补丁包

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
public class MainActivity extends AppCompatActivity {
private String mPath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mPath = getExternalCacheDir().getAbsolutePath() + File.separatorChar;
}
/**
* 加载Tinker补丁
*
* @param view
*/
public void Fix(View view) {
File patchFile = new File(mPath, "patch_signed.apk");
if (patchFile.exists()) {
TinkerManager.addPatch(patchFile.getAbsolutePath());
Toast.makeText(this, "File Exists,Please wait a moment ", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "File No Exists", Toast.LENGTH_SHORT).show();
}
}
}

Tinker测试

完成Tinker的所有准备工作后,我们通过默认的初始化Tinker方式测试我们的补丁包

1、找到gradle工具栏,点击生成Release包,作为1.0版本的程序

这里写图片描述

2、将生成的Release包Push到手机上,安装,运行程序

生成apk的目录在build的bakApk目录下

这里写图片描述

运行程序

这里写图片描述

3、在项目中,对主界面添加加载图片的按钮,同时添加一个drawable文件

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
public class MainActivity extends AppCompatActivity {
private String mPath;
private ImageView iv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = (ImageView) findViewById(R.id.iv);
mPath = getExternalCacheDir().getAbsolutePath() + File.separatorChar;
}
/**
* 加载Tinker补丁
*
* @param view
*/
public void Fix(View view) {
File patchFile = new File(mPath, "patch_signed.apk");
if (patchFile.exists()) {
TinkerManager.addPatch(patchFile.getAbsolutePath());
Toast.makeText(this, "File Exists,Please wait a moment ", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "File No Exists", Toast.LENGTH_SHORT).show();
}
}
/**
* 新增的按钮点击事件
*
* @param view
*/
public void Load(View view) {
iv.setImageResource(R.drawable.bg_content_header);
}
}

4、同时记得修改buildTinker.gradle的old安装包的路径,Tinker需要比对前后安装包然后生成补丁包

1
2
3
4
5
6
7
8
9
10
11
12
//参数配置
ext {
//开启Tinker
tinkerEnable = true
//旧的apk位置,需要我们手动指定
tinkerOldApkPath = "${bakPath}/app-release-2017-11-19-18-34-12.apk"
//旧的混淆映射位置,如果开启了混淆,则需要我们手动指定
tinkerApplyMappingPath = "${bakPath}/"
//旧的resource位置,需要我们手动指定
tinkerApplyResourcePath = "${bakPath}/app-release-2017-11-19-18-34-12-R.txt"
tinkerID = "1.0"
}

5、找到gradle工具栏,点击thinker生成Release补丁包,作为1.0版本的补丁

这里写图片描述

6、将生成的Release补丁包Push到手机的缓存目录上,运行程序点击修复补丁包,稍等数秒程序会被杀掉,重启点击加载图片按钮

生成的补丁包

这里写图片描述

记得将补丁放到缓存目录下,修复补丁后的程序

这里写图片描述

Tinker多渠道打包

1、Tinker支持多渠道打包,我们采用友盟的打包方式,下载友盟SDK,将jar包增加到项目上

这里写图片描述

2、初始化友盟SDK(新版本的SDK似乎不用初始化了,找不到初始化入口)

3、Manifest增加友盟的AppKey配置和渠道配置

1
2
3
4
5
6
<meta-data
android:name="UMENG_APPKEY"
android:value="5a116bbea40fa33cf9000150" />
<meta-data
android:name="UMENG_CHANNEL"
android:value="${UMENG_CHANNEL_VALUE}" />

4、在app的build.gradle中增加多渠道打包信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 配置多渠道
*/
productFlavors {
googleplayer {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "googleplayer"]
}
baidu {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"]
}
wangdoujia {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wangdoujia"]
}
productFlavors.all {
flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
}

5、在buildTinker.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
/**
* 生成多渠道补丁包
*/
project.afterEvaluate {
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"
}
}
}
}
}

6、找到gradle工具栏,点击生成Release包,作为1.0版本的程序

这里写图片描述

7、同时记得修改buildTinker.gradle的old安装包的路径,Tinker需要比对前后安装包然后生成补丁包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//参数配置
ext {
//开启Tinker
tinkerEnable = true
//旧的apk位置,需要我们手动指定
tinkerOldApkPath = "${bakPath}/app-2017-11-19-20-35-23"
//旧的混淆映射位置,如果开启了混淆,则需要我们手动指定
tinkerApplyMappingPath = "${bakPath}/app-2017-11-19-20-35-23"
//旧的resource位置,需要我们手动指定
tinkerApplyResourcePath = "${bakPath}/app-2017-11-19-20-35-23"
//旧的多渠道位置,需要我们手动指定
tinkerBuildFlavorDirectory = "${bakPath}/app-2017-11-19-20-35-23"
tinkerID = "1.0"
}

8、找到gradle工具栏,点击thinker生成Release补丁包,作为1.0版本的补丁

这里对程序的修改就省略了

这里写图片描述

后面的测试更上面一样,也就省略了

源码下载

源码下载

结语

当野心大于现实的能力,只能默默的学习提升自己的能力,互相努力吧

坚持原创技术分享,您的支持将鼓励我继续创作!