Android热修复方案(二)实践篇

概述

经过上一轮《Android热修复方案(一)理论篇》关于热修复的理论介绍,这篇重点实践一下该理论。这是基于Tinker的热修复。

具体实现

新建项目,添加依赖

创建一个全新的项目TinkerTest。先在Project的gradle中引用库。在app的gradle添加依赖:
project:

1
2
3
4
5
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
//无需再单独引用tinker的其他库
classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:${TINKERPATCH_VERSION}"
}

app:

1
2
3
4
5
6
7
8
9
10
11
12
13
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "com.android.support:appcompat-v7:26.0.1"
implementation "com.android.support:multidex:1.0.2"
//若使用annotation需要单独引用,对于tinker的其他库都无需再引用
annotationProcessor("com.tinkerpatch.tinker:tinker-android-anno:${TINKER_VERSION}") {
changing = true
}
compileOnly("com.tinkerpatch.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
implementation("com.tinkerpatch.sdk:tinkerpatch-android-sdk:${TINKERPATCH_VERSION}") {
changing = true
}
}

properties:

1
2
TINKER_VERSION=1.9.2
TINKERPATCH_VERSION=1.2.2

配置信息

可参照官方给demo来进行配置。把必要的信息修改为本项目的,具体见注释

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
/**
* TODO: 请按自己的需求修改为适应自己工程的参数
*/
def bakPath = file("${buildDir}/bakApk/")
def baseInfo = "app-1.0.0-0510-16-22-11"//对应着有Bug的apk来生成补丁
def variantName = "debug"

/**
* 对于插件各参数的详细解析请参考
* http://tinkerpatch.com/Docs/SDK
*/
tinkerpatchSupport {
/** 可以在debug的时候关闭 tinkerPatch **/
/** 当disable tinker的时候需要添加multiDexKeepProguard和proguardFiles,
这些配置文件本身由tinkerPatch的插件自动添加,当你disable后需要手动添加
你可以copy本示例中的proguardRules.pro和tinkerMultidexKeep.pro,
需要你手动修改'tinker.sample.android.app'本示例的包名为你自己的包名, com.xxx前缀的包名不用修改
**/
tinkerEnable = true
reflectApplication = false
/**
* 是否开启加固模式,只能在APK将要进行加固时使用,否则会patch失败。
* 如果只在某个渠道使用了加固,可使用多flavors配置
**/
protectedApp = false
/**
* 实验功能
* 补丁是否支持新增 Activity (新增Activity的exported属性必须为false)
**/
supportComponent = true

autoBackupApkPath = "${bakPath}"

appKey = "你的key"//可不配置

/** 注意: 若发布新的全量包, appVersion一定要更新 **/
appVersion = "1.0.0"

def pathPrefix = "${bakPath}/${baseInfo}/${variantName}/"
def name = "${project.name}-${variantName}"

baseApkFile = "${pathPrefix}/${name}.apk"
baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
baseResourceRFile = "${pathPrefix}/${name}-R.txt"

/**
* 若有编译多flavors需求, 可以参照: https://github.com/TinkerPatch/tinkerpatch-flavors-sample
* 注意: 除非你不同的flavor代码是不一样的,不然建议采用zip comment或者文件方式生成渠道信息(相关工具:walle 或者 packer-ng)
**/
}

/**
* 用于用户在代码中判断tinkerPatch是否被使能
*/
android {
defaultConfig {
buildConfigField "boolean", "TINKER_ENABLE", "${tinkerpatchSupport.tinkerEnable}"
}
}

/**
* 一般来说,我们无需对下面的参数做任何的修改
* 对于各参数的详细介绍请参考:
* https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
*/
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
}
}

编写有Bug的代码

虚拟一个线上的Bug
activity_main.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent"
android:gravity="center"
tools:context=".MainActivity">

<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="我是bug"
/>
<Button
android:id="@+id/fixBugbBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="修复bug"
/>

</LinearLayout>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void TestTinker() {
Toast.makeText(this,Environment.getExternalStorageDirectory().getAbsolutePath() +
"/patch_signed_7zip.apk",Toast.LENGTH_LONG).show();
fixBugBtn = findViewById(R.id.fixBugbBtn);
textView = findViewById(R.id.text);
//textView.setText("我已修复");假设textView上的文本是Bug,去掉注释代表修复了该Bug
//fixBugBtn.setVisibility(View.INVISIBLE);

fixBugBtn.setOnClickListener(new View.OnClickListener() {//加载补丁
@Override
public void onClick(View v) {

TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),
Environment.getExternalStorageDirectory().getAbsolutePath() +
"/patch_signed_7zip.apk");//等下要push到SD卡里面去apk,以达到更新的目的

}
});
}

业务非常简单,textView上出现了Bug,待修复。

安装有Bug的apk

把有问题的apk安装进设备,生成的apk的时候同时也会在bakapk下生成对应的Tag版本。后续打补丁的时候也要对应该版本

修复Bug

更改TextView上的文本,隐藏加载补丁的按钮

1
2
textView.setText("我已修复");//去掉注释代表修复了该Bug
fixBugBtn.setVisibility(View.INVISIBLE);//去掉注释

生成补丁

生成补丁之前一定要设置自己的工程参数:

修改完代码之后,如果是debug就点击”tinkerPacthDeBug“,如果是Release就点击”tinkerPacthRelease“

最终的补丁会放在output下,以patch_signed_7zip.apk命名。

加载补丁

需要将补丁放到目标目录下,例如放入根目录,可以用USB先拷贝,也可以用adb指令的方式。
用adb来push补丁

最终实现效果


可以看出加载补丁后按钮消失,文本由”我是bug“变成”我已修复“

总结

总的来说使用Tinker并不难,官网还提供了傻瓜式的一键接入的方式,用起来十分方便。但是还是要理解他的原理,这很重要。最后再赘述一次:
Tinker是用了类加载的形式进行热修复,主要将旧apk(有bug)和新apk(无bug)进行比对生成新的补丁。新补丁包含了目标dex文件,tinker将其放到Element集合的第一位优先加载。根据双亲委派模型就不会加载后面有bug的类了。最终达到了类的替换目的。

源码下载

点我查看/下载源码