前言
不知你是否遇到這樣的情況?千辛萬苦上開發(fā)了一個版本,好不容易上線了,突然發(fā)現(xiàn)了一個嚴重bug需要進行緊急修復,怎么辦?難道又要重新打包App、測試,發(fā)布新個版本?就為了修改一兩行的代碼?
莫慌,這種問題其實可以分分鐘解決。如果你學會了這項黑科技——熱修復。
在用戶使用App的時候,不知不覺,這個Bug就被修復了。

熱修復:熱修復(也稱熱補丁、熱修復補丁,英語:hotfix)是一種包含信息的獨立的累積更新包,通常表現(xiàn)為一個或多個文件。這被用來解決軟件產(chǎn)品的問題(例如一個程序錯誤)?!S基百科
本文介紹了Tinker的接入方式,更加詳細的內(nèi)容可以查閱官方文檔
介紹
Tinker是微信官方的Android熱補丁解決方案,它支持動態(tài)下發(fā)代碼、So庫以及資源,讓應用能夠在不需要重新安裝的情況下實現(xiàn)更新。當然,你也可以使用Tinker來更新你的插件。
Tinker所支持的功能如下

Tinker熱補丁方案·不僅支持類、So以及資源的替換,它還是2.X-7.X的全平臺支持。
接入
進入正題吧

在項目的
build.gradle中
buildscript {
repositories {
jcenter()
}
dependencies {
// TinkerPatch 插件
classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:1.1.4"
}
}
然后在app的gradle文件app/build.gradle中
dependencies {
// 若使用annotation需要單獨引用,對于tinker的其他庫都無需再引用
provided("com.tencent.tinker:tinker-android-anno:1.7.7")
compile("com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.1.4")
}
在app目錄下,創(chuàng)建tinkerpatch.gradle(可以去后面的鏈接下載源碼,把這個文件拷進去)

將 TinkerPatch 相關的配置都放于
tinkerpatch.gradle中,然后在app的gradle文件app/build.gradle中還添加
apply from: 'tinkerpatch.gradle'
完整的app/buidl.gradle:
apply plugin: 'com.android.application'
apply from: 'tinkerpatch.gradle'
android {
...
}
dependencies {
.....
// 若使用annotation需要單獨引用,對于tinker的其他庫都無需再引用
provided("com.tencent.tinker:tinker-android-anno:1.7.7")
compile("com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.1.4")
}
配置參數(shù)
打開之前創(chuàng)建的tinkerpatch.gradle添加
apply plugin: 'tinkerpatch-support'
/**
* TODO: 請按自己的需求修改為適應自己工程的參數(shù)
*/
def bakPath = file("${buildDir}/bakApk/")
def baseInfo = "app-1.0.0-0330-20-37-31"
def variantName = "release"
/**
* 對于插件各參數(shù)的詳細解析請參考
* http://tinkerpatch.com/Docs/SDK
*/
tinkerpatchSupport {
/** 可以在debug的時候關閉 tinkerPatch **/
tinkerEnable = true
reflectApplication = true
autoBackupApkPath = "${bakPath}"
appKey = "你的appkey"http:// 注意?。?! 需要修改成你的appkey
/** 注意: 若發(fā)布新的全量包, 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}"
}
}
/**
* 一般來說,我們無需對下面的參數(shù)做任何的修改
* 對于各參數(shù)的詳細介紹請參考:
* 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
}
}
- 參數(shù)說明
- bakPath:基包路徑
- baseInfo:基包文件夾名(打補丁包的時候,需要修改)
- appKey:進入官網(wǎng)注冊一個賬號,新增APP,得到對應的appKey。
官方參數(shù)說明
其中
appKey在新增的APP中可以看到
appKey
使用
創(chuàng)建FetchPatchHandler用于檢測是否更新(剛打開時會檢測一次)
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);
}
}
創(chuàng)建MyApplication初始化(reflectApplication = true 時)
public class MyApplication extends Application{
private ApplicationLike tinkerApplicationLike;
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.TINKER_ENABLE) {
// 我們可以從這里獲得Tinker加載過程的信息
tinkerApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike();
// 初始化TinkerPatch SDK, 更多配置可參照API章節(jié)中的,初始化SDK
TinkerPatch.init(tinkerApplicationLike)
.reflectPatchLibrary()
.setPatchRollbackOnScreenOff(true)
.setPatchRestartOnSrceenOff(true);
// 每隔3個小時去訪問后臺時候有更新,通過handler實現(xiàn)輪訓的效果
new FetchPatchHandler().fetchPatchWithInterval(3);
}
}
}
最后在記得在Manifest.xml中配置Application,還要給SD卡讀寫權限
<manifest >
.....
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
...
android:name=".MyApplication">
</manifest>
注意:初始化的代碼建議緊跟 super.onCreate(),并且所有進程都需要初始化,已達到所有進程都可以被 patch 的目的
如果你確定只想在主進程中初始化 tinkerPatch,那也請至少在 :patch 進程中初始化,否則會有嚴重的 crash 問題
打生產(chǎn)包
注意:打包前記得配置簽名??蓞⒖荚创a
每次開發(fā)完成后,開始打包。
打開Studio右側的Gradle,選擇assemableRelease打正式包

完成后可以在文件夾build中找到生成的文件(這里稱為基包)

打開build -> bakApk -> app-1.0.0-0330-21-40-52 (根據(jù)時間命名)
release文件夾中會出現(xiàn)我們剛打完的包。一個apk,對應一個txt文件。
將apk安裝到手機上,這是一個剛創(chuàng)建的項目,里面只有Hello World

將
app-1.0.0-0330-21-40-52備份,打補丁包的時候需要用到。
多渠道打包時會根據(jù)渠道名分包,目錄結構相似。
生成補丁包
這里模擬一個修補bug的場景,發(fā)一個熱補丁,彈一個Toast。
注意:在打生產(chǎn)包的代碼上做修改
Toast.makeText(this, "這是個補丁", Toast.LENGTH_SHORT).show();
-
將之前的備份好的基包復制到
build/bakApk下,打開tinkerpatch.gradle修改baseInfo成對應的文件名
修改baseInfo -
修改
tinkerpatch.gradle中的tinkerpatchSupport->appVersion
appVersion -
完成后打開
Gradle,如下選擇tinkerPatchRelease
Gradle
補丁包將位于 build/outputs/tinkerPatch 中,這里只需要用到patch_signed_7zip.apk
補丁包
發(fā)布
最后,只需要將剛生成的補丁包發(fā)布,然后靜靜等待即可。
- 進入官方網(wǎng)站,新增
APP
添加APP
- 選擇對應的app,添加APP版本
添加APP版本
添加版本
版本號對應tinkerpatch.gradle中的appVersion - 選擇
patch_signed_7zip.apk文件,提交即可(更多下發(fā)選項,參考官方文檔)
發(fā)布補丁 -
提交后,查看補丁的下載數(shù)量以及成功應用數(shù)
補丁詳情
這時候重新打開app,等待補丁下載。下載完成后關閉app,再次打開,查看結果。就這樣,整個熱修復的流程就完成了。
注意:一定要關閉后打開,熱修復才會生效。

關于兼容多渠道包
關于渠道包的問題,若使用flavor編譯渠道包,會導致不同的渠道包由于BuildConfig變化導致classes.dex差異。這里建議的方式有:
- 將渠道信息寫在
AndroidManifest.xml或文件中,例如channel.ini; - 將渠道信息寫在
apk文件的zip comment中,這種是建議方式,例如可以使用項目packer-ng-plugin或者可使用V2 Scheme的walle; - 若不同渠道存在功能上的差異,建議將差異部分放于單獨的dex或采用相同代碼不同配置方式實現(xiàn);
已通過Walle實現(xiàn):【Android】Walle多渠道打包&Tinker熱修復
Tinker已知的問題:
- Tinker不支持修改
AndroidManifest.xml,Tinker不支持新增四大組件; - 由于Google Play的開發(fā)者條款限制,不建議在GP渠道動態(tài)更新代碼;
- 在
Android N上,補丁對應用啟動時間有輕微的影響; - 不支持部分三星android-21機型,加載補丁時會主動拋出
TinkerRuntimeException:checkDexInstall failed; - 由于各個廠商的加固實現(xiàn)并不一致,在1.7.6以及之后的版本,tinker不再支持加固的動態(tài)更新;
(在官方——對加固的支持看到相關的內(nèi)容,說是支持多種加固,不過我沒試過 2017.7.3) - 對于資源替換,不支持修改
remoteView。例如transition動畫,notification icon以及桌面圖標。
Tips:
-
dubug模式下,tinkerpatch.gradle—>tinkerpatchSupport—>tinkerEnable需要改為false - 添加
SD卡權限, - 下載補丁后,殺掉進程重新打開,補丁才會生效
- 補丁非即時生效,需要等一會兒
源碼地址
參考
以上有錯誤之處,感謝指出










