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用於線上產品的功能升級應該沒什麼問題,其它更高級的用法自行深入研究;

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章