關于熱更新目前的各種方案原理看我之前這篇文章的介紹熱補丁動態修復技術調研
這篇文章將選擇Trinker集成體驗一下熱更新。
1.什么是Tinker?
Tinker 是一個開源項目(Github鏈接),它是微信官方的 Android 熱補丁解決方案,它支持動態下發代碼、So 庫以及資源,讓應用能夠在不需要重新安裝的情況下實現更新。但需要注意的是需要重啟Activity或重啟Application才能達到更新效果。
2.對比其他以及選擇Tinker的理由?
將Tinker與其他原理的熱補丁方案做對比:阿里的 AndFix、美團的 Robust 以及 QZone 的超級補丁方案,如果不了解這三類原理,就看這篇文章的介紹熱補丁動態修復技術調研
總的來說:
-
AndFix作為native解決方案,首先面臨的是穩定性與兼容性問題,更重要的是它無法實現類替換,它是需要大量額外的開發成本的;
-
Robust兼容性與成功率較高,但是它與AndFix一樣,無法新增變量與類只能用做的bugFix方案;
-
Qzone方案可以做到發布產品功能,但是它主要問題是插樁帶來Dalvik的性能問題,以及為了解決Art下內存地址問題而導致補丁包急速增大的。
Tinker熱補丁方案不僅支持類、So 以及資源的替換,它還是2.X-7.X的全平臺支持。利用Tinker我們不僅可以用做 bugfix,甚至可以替代功能的發布。Tinker 已運行在微信的數億 Android 設備上,那么為什么你不使用 Tinker 呢?
3.接入TinkerPatch 平臺
- Tinker 需要使用者有一個后臺可以下發和管理補丁包,并且需要處理傳輸安全等部署工作,TinkerPatch 平臺幫你做了這些工作,提供了補丁后臺托管,版本管理,保證傳輸安全等功能,讓你無需搭建一個后臺,無需關心部署操作,只需引入一個 SDK 即可立即使用 Tinker。
- 接入平臺,無需理解復雜的熱修復原理,一行代碼即可接入熱修復。實現了自動反射 Appliction 與 Library,使用者無需對自己的項目做任何的改動;
- 當然如果我們要更好的理解tinker,需要研究其原理,源碼,若不使用TinkerPatch平臺,也可以根據需求自己搭建后臺,根據開源的tinker自行接入。
4.【通過接入TinkerPatch 平臺,開啟Tinker之旅】
第一步:注冊 TinkerPatch 平臺賬號點擊前往注冊
第二步 添加 APP
名字只是一個標識,沒有其他要求
第三步 記錄 AppKey
添加完APP,會生成一個AppKey,這個將在后續代碼中使用
第四步 客戶端SDK接入 【重點】
1.添加 gradle 插件依賴 :位于【工程】的build.gradle下,gradle 遠程倉庫依賴 jcenter。
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
// 添加TinkerPatch 插件
//無需再單獨引用tinker的其他庫
classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:1.2.2"
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
2.集成 TinkerPatch SDK, 位于【app】的build.gradle下
dependencies {
...
compile 'com.android.support:multidex:1.0.1'
// 若使用annotation需要單獨引用,對于tinker的其他庫都無需再引用
provided("com.tinkerpatch.tinker:tinker-android-anno:1.9.2")
compile "com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.2.2"
}
對于有的使用的as3.0,引入語法有變化,如下配置
dependencies {
...
implementation 'com.android.support:multidex:1.0.1'
// 若使用annotation需要單獨引用,對于tinker的其他庫都無需再引用
compileOnly("com.tinkerpatch.tinker:tinker-android-anno:1.9.2")
implementation "com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.2.2"
}
注意,若使用 annotation 自動生成 Application, 需要單獨引入 Tinker 的 tinker-android-anno 庫。除此之外,我們無需再單獨引入 tinker 的其他庫。
同時在【app】目錄下需要將 TinkerPatch 相關的配置都放于 tinkerpatch.gradle 中,所以新建一個file,命名為tinkerpatch.gradle
所以我們需要在app下的build.gradle引入
dependencies {
...
}
//添加
apply from: 'tinkerpatch.gradle'
3.配置 tinkerpatchSupport 參數 ,在tinkerpatch.gradle下
apply plugin: 'tinkerpatch-support'
//這里需要說明bakPath定義了基準包的輸出位置
def bakPath = file("${buildDir}/bakApk/")
//baseInfo和variantName 對于基準包我們可以忽略不填,
//因為tinker也不會對這個做處理
//它主要的作用是在打補丁包是,我們要根據基準包輸出的路徑下的信息做修改。主要是給補丁包配置
//這里我們先默認為"",等到基準包完成后再修改。
def baseInfo = "xxx"
def variantName = "xxx"
tinkerpatchSupport {
tinkerEnable = true
reflectApplication = true
protectedApp = true
supportComponent = false
autoBackupApkPath = "${bakPath}"
appKey = "d8de698f2ac404aa"
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"
}
android {
defaultConfig {
buildConfigField "boolean", "TINKER_ENABLE", "${tinkerpatchSupport.tinkerEnable}"
}
}
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"
}
buildConfig {
keepDexApply = false
}
}
重要參數描述
我們將原apk包稱為基準apk包,tinkerPatch直接使用基準apk包與新編譯出來的apk包做差異,得到最終的補丁包。
參數 | 默認值 | 描述 | 其他說明 |
---|---|---|---|
tinkerEnable | true | 是否開啟 tinkerpatchSupport 插件功能。 | 在開啟時將不能instant run,所以若想調試程序,在debug的時候關閉false |
reflectApplication | false | 是否反射 Application 實現一鍵接入 | 一般來說,接入 Tinker 我們需要改造我們的 Application, 若這里為 true, 即我們無需對應用做任何改造即可接入。 |
protectedApp | false | 是否開啟支持加固 | 若我們的程序需要加固,則開啟此選項 |
supportComponent | false | 是否開啟支持在補丁包中動態增加Activity | 注意:新增Activity的Exported屬性必須為false? |
autoBackupApkPath | "" | 指定每次編譯產生的 apk/mapping.txt/R.txt 歸檔存儲的位置 | 無 |
appKey | "" | 在 TinkerPatch 平臺 申請的 appkey | 就是我們在上面提到的appkey |
appVersion | "" | 和 TinkerPatch 平臺 的版本號需要對應,若這里我們輸入1.0.0,之后在TinkerPatch平臺上我們也需要輸入1.0.0 | 注意,我們使用 appVersion 作為 TinkerId, 我們需要保證每個發布出去的基礎安裝包的 appVersion 都不一樣。 |
baseApkFile | "" | 基準包的文件路徑,對應 tinker 插件中的 oldApk 參數 | 編譯補丁包時,必需指定基準版本的 apk(路徑以及名字,如果不對應,將不到找到對應的基準包進行打補丁操作),默認值為空,則表示不是進行補丁包的編譯。所以這個參數很重要。 |
baseProguardMappingFile | "" | 基準包的 Proguard mapping.txt 文件路徑, 對應 tinker 插件 applyMapping 參數 | 在編譯新的 apk 時候,我們希望通過保持基準 apk 的 proguard 混淆方式,從而減少補丁包的大小。這是強烈推薦的,編譯補丁包時,我們推薦輸入基準 apk 生成的 mapping.txt 文件。 |
baseResourceRFile | "" | 基準包的資源 R.txt 文件路徑, 對應 tinker 插件 applyResourceMapping 參數 | 在編譯新的apk時候,我們希望通基準 apk 的 R.txt 文件來保持 Resource Id 的分配,這樣不僅可以減少補丁包的大小,同時也避免由于 Resource Id 改變導致 remote view 異常。 |
tinkerPatch | 全局信息相關的配置項 | 一般來說,我們無需對其中的參數做任何的修改 | |
outputFolder | 設置編譯輸出路徑,也就是補丁包生成的位置,默認在build/outputs/tinkerPatch中 | 一般我們不做指定,也不用配置,只需要知道補丁生成的位置在哪里 | |
ignoreWarning | false | 如果出現如右側的五種情況,并且ignoreWarning為false,程序將中斷編譯。因為這些情況可能會導致編譯出來的patch包帶來風險 | 1. minSdkVersion小于14,但是dexMode的值為"raw";2. 新編譯的安裝包出現新增的四大組件(Activity, BroadcastReceiver...);3. 定義在dex.loader用于加載補丁的類不在main dex中;4. 定義在dex.loader用于加載補丁的類出現修改;5. resources.arsc改變,但沒有使用applyResourceMapping編譯。 |
useSign | ture | 在運行過程中,我們需要驗證基準apk包與補丁包的簽名是否一致,我們是否需要為你簽名。 | 做好不要做修改 |
4.初始化 TinkerPatch SDK
最后在我們的代碼中,只需簡單的初始化 TinkerPatch 的 SDK 即可,我們無需考慮 Tinker 是如何下載/合成/應用補丁包, 也無需引入各種各樣 Tinker 的相關類。
對于初始化操作我們分為兩種情況:
-
第一種是我們的 reflectApplication = true,即一鍵接入,此時我們無需為接入 Tinker 而改造我們的 Application 類。
public class MyApplication extends Application {
...
@Override
public void onCreate() {
super.onCreate();
// 我們可以從這里獲得Tinker加載過程的信息
ApplicationLike tinkerApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike();
// 初始化TinkerPatch SDK, 更多配置可參照API章節中的,初始化SDK
TinkerPatch.init(tinkerApplicationLike)
.reflectPatchLibrary()
.setPatchRollbackOnScreenOff(true)
.setPatchRestartOnSrceenOff(true)
.setFetchPatchIntervalByHours(3);
// 每隔3個小時(通過setFetchPatchIntervalByHours設置)去訪問后臺時候有更新,通過handler實現輪訓的效果
TinkerPatch.with().fetchPatchUpdateAndPollWithInterval();
}
...
-
第二種是我們的 reflectApplication = false,就是根據自己的需求自己改造應用的 Application。參考 tinkerpatch-sample 中的 SampleApplicationLike 類這里不做更多說明。
5.使用步驟
到此為止,我們的配置已經完成,就剩下實踐操作了:
1.獲得基準包
- 確保更新tinkerpatchSupport中的appVersion
- 運行 assembleRelease task 構建基準包。也就是我們通常通過命令./gradlew assembleRelease 出的apk包。
- tinkerPatch會基于你填入的autoBackupApkPath自動備份基礎包信息到相應的文件夾,包含:apk文件、R.txt文件和mapping.txt文件 (注:mapping.txt是proguard的產物,如果你沒有開啟proguard則不會有這個文件?)
-
根據我們的配置,此時構建完成基準包的位置即為如圖
基準包 - tinker按照時間戳給我們命名
- 你要發布的apk就是該路徑下的apk,如果需要加固也是用該apk。
2.獲得補丁包
- 構建之前:將自動保存下來的文件分別填到tinkerpatchSupport中的baseApkFile、baseProguardMappingFile和baseResourceRFile 參數中,因為我們需要根據這些參數路徑對應的apk和當前工程對比出不同,然后打出補丁。
-
修改配置文件【重點】:
- 所以你應該理解了,每次打補丁只需要改根據基準apk修改這兩處就可以了,baseApkFile、baseProguardMappingFile和baseResourceRFile 也就自動改變了
def pathPrefix = "${bakPath}/${baseInfo}/${variantName}"
def name = "${project.name}-${variantName}"
baseApkFile = "${pathPrefix}/${name}.apk"
baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
baseResourceRFile = "${pathPrefix}/${name}-R.txt"
-
最后運行 tinkerPatchRelease task 構建補丁包,若無渠道,可通過./gradlew tinkerPatchRelease構建,我這個有一個小米渠道,可以通過./gradlew tinkerPatchXiaomiRelease構建。或者最簡單的方式如圖:
-
完成后,補丁包將位于 build/outputs/tinkerPatch下。
3.發布補丁包
- 將剛所得的補丁包patch_signed_7zip.apk重命名,去掉.apk后綴,我將其命名為patch_signed。
-
然后回到tinker平臺上,找到我們的app,然后新建版本,我們代碼中的appVersion為1.0.0
-
上傳補丁包并下發:
4.重新安裝我們的基準包,然后刷新tinker后臺
5.殺掉進程,重新啟動。補丁加載成功
成功接入了~給個小心心鼓勵一下吧