熱修復——Tinker 的整合與使用
作者 | CSDN_LQR
地址 | https://juejin.im/post/5a27bdaf6fb9a044fa19bcfc
宣告 | 本文是 CSDN_LQR 原創,已獲授權釋出,未經原作者允許請勿轉載
簡述
Tinker是微信官方的Android熱補丁解決方案,它支援動態下發程式碼、So庫以及資源,讓應用能夠在不需要重新安裝的情況下實現更新。當然,你也可以使用Tinker來更新你的外掛。
上面是Tinker官方Wiki的原話,意思嘛相信大家都看得明白,但注意啦,它並沒有說Tinker可以讓補丁實時生效(也叫無感知更新),它必須在打上補丁後重啟App(重啟程序),補丁才會發揮作用,這跟阿里的熱修復方案有著本質的區別。在開始整合Tinker之前,我們有必要了解清楚,Tinker有那些不足,下面是Tinker的已知問題:
Tinker不支援修改AndroidManifest.xml,Tinker不支援新增四大元件(1.9.0支援新增非export的Activity);
由於Google Play的開發者條款限制,不建議在GP渠道動態更新程式碼;
在Android N上,補丁對應用啟動時間有輕微的影響;
不支援部分三星android-21機型,載入補丁時會主動丟擲"TinkerRuntimeException:checkDexInstall failed";
對於資源替換,不支援修改remoteView。例如transition動畫,notification icon以及桌面圖示。
上述不足是由於原理與系統限制,我們在程式設計中要清楚這些,儘量避免以上問題的出現。
儘管Tinker有著這些“小缺點”,但也絲毫不影響Tinker在國內眾多熱修復方案中的地位,一方面Tinker是開源的(這意味著Tinker本身免費),另一方面則是Tinker已執行在微信的數億Android裝置上(說明該方案相當穩定)。下面開始進行對Tinker的整合與使用。
Tinker元件依賴
1、在專案的build.gradle中:
新增tinker-patch-gradle-plugin的依賴
buildscript {
dependencies {
classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1' )
}
}
2、在app的gradle檔案(app/build.gradle)中:
需要注意一點,Tinker需要使用到MulitDex,原話在Bugly文件的熱更新API介面部分。https://bugly.qq.com/docs/user-guide/api-hotfix/?v=20170504092424
1)新增tinker的庫依賴
Gradle版本小於2.3的這麼寫:
dependencies {
compile "com.android.support:multidex:1.0.1"
//可選,用於生成application類
provided('com.tencent.tinker:tinker-android-anno:1.9.1')
//tinker的核心庫
compile('com.tencent.tinker:tinker-android-lib:1.9.1')
}
Gradle版本大等於2.3的這麼寫:
dependencies {
implementation "com.android.support:multidex:1.0.1"
//tinker的核心庫
implementation("com.tencent.tinker:tinker-android-lib:1.9.1") { changing = true }
//可選,用於生成application類
annotationProcessor("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }
compileOnly("com.tencent.tinker:tinker-android-anno:1.9.1") { changing = true }
}
2)開啟multiDex
defaultConfig {
...
multiDexEnabled true
}
3)應用tinker的gradle外掛
這部分可先不管,在第三部分《Tinker的配置及任務》的第2節《配置Tinker與任務》中會新增。可跳過這部分繼續往下看。
//apply tinker外掛
apply plugin: 'com.tencent.tinker.patch
Tinker的配置及任務
1、開啟支援大工程模式
Tinker文件中推薦將jumboMode設定為true。
android {
dexOptions {
// 支援大工程模式
jumboMode = true
}
...
}
2、配置Tinker與任務
將下面的配置全部複製貼上到app的gradle檔案(app/build.gradle)末尾,內容很多,但現在只需要看懂bakPath與ext括號內的東東就好了。
// Tinker配置與任務
def bakPath = file("${buildDir}/bakApk/")
ext {
// 是否使用Tinker(當你的專案處於開發除錯階段時,可以改為false)
tinkerEnabled = true
// 基礎包檔案路徑(名字這裡寫死為old-app.apk。用於比較新舊app以生成補丁包,不管是debug還是release編譯)
tinkerOldApkPath = "${bakPath}/old-app.apk"
// 基礎包的mapping.txt檔案路徑(用於輔助混淆補丁包的生成,一般在生成release版app時會使用到混淆,所以這個mapping.txt檔案一般只是用於release安裝包補丁的生成)
tinkerApplyMappingPath = "${bakPath}/old-app-mapping.txt"
// 基礎包的R.txt檔案路徑(如果你的安裝包中資原始檔有改動,則需要使用該R.txt檔案來輔助生成補丁包)
tinkerApplyResourcePath = "${bakPath}/old-app-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/flavor"
}
def getOldApkPath() {
return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}
其中,有幾點配置在這裡說明一下,方便理解後續的操作(當tinkerEnabled = true的情況下):
app的生成目錄是:主Module(一般是名為app)/build/bakApk資料夾。
補丁包的生成路徑:主Module(一般是名為app)/build/outputs/apk/tinkerPatch/debug/patch_signed_7zip.apk。
基礎包的名字:old-app.apk,放於bakApk資料夾下。
基礎包的mapping.txt和R.txt檔案一般在編譯release簽名的apk時才會用到。
在用到mapping.txt檔案時,需要重新命名為old-app-mapping.txt,放於bakApk資料夾下。
在用到R.txt檔案時,需要重新命名為old-app-R.txt,放於bakApk資料夾下。
對於mapping.txt和R.txt檔案,在配置中有說明,請回配置中仔細看。
上面只是我專案中的配置,這些其實都是可以自定義的,建議在搞清楚配置內容之後再去自定義修改。
什麼是基礎包??
基礎包就是已經上架的apk檔案(假設是1.0版本)。這其實很好理解,在新版本的App上架之前(假設是2.0版本),我們會用到Tinker來修復1.0版App中存在的bug,這時就需要用到Tinker來產生補丁包檔案,而補丁包檔案的本質,就是修復好Bug的App與1.0版本App之間的檔案差異。在2.0版本上架之前,我們可能會多次產生新的補丁包,用於修復在使用者手機上的1.0版App,所以補丁包必須以1.0版App作為參考標準,也就是說使用者手機上的app就是基礎包,即當前應用市場上的apk檔案(前面說的1.0版本)。
Tinker 封裝與拓展
1、拷貝檔案
將Demo中提供的tinker包下的所有檔案及資料夾都拷貝到自己專案中。
這些檔案其實就是Tinker官方Demo中的檔案完全複製過來的,只是多加了一些註釋。
簡單說明下,這幾個檔案的作用:
SampleUncaughtExceptionHandler:Tinker的全域性異常捕獲器。
MyLogImp:Tinker的日誌輸出實現類。
SampleLoadReporter:載入補丁時的一些回撥。
SamplePatchListener:過濾Tinker收到的補丁包的修復、升級請求。
SamplePatchReporter:修復或者升級補丁時的一些回撥。
SampleTinkerReport:修復結果(成功、衝突、失敗等)。
SampleResultService::patch補丁合成程序將合成結果返回給主程序的類。
TinkerManager:Tinker管理器(安裝、初始化Tinker)。
TinkerUtils:拓展補丁條件判定、鎖屏或後臺時應用重啟功能的工具類。
這些只是對Tinker功能的拓展和封裝罷了,都是可選的,但這些檔案對專案的功能完善會有所幫助,建議加入到自己的專案中。
如果你僅僅只是為了修復bug,而不做過多的工作(如:上傳打補丁資訊到伺服器等),則無須理會這些檔案的作用,當然你也可以自己封裝。
對於這些自定義類及錯誤碼的詳細說明,請參考:「Tinker官方Wiki:可選的自定義類」。
2、清單檔案中新增服務
前面新增的檔案中,有一個SampleResultService檔案,是四大元件之一,所以必須在清單檔案中宣告。
<service
android:name="com.lqr.tinker.service.SampleResultService"
android:exported="false"/>
編寫Application的代理類
Tinker表示,Application無法動態修復,所以有兩種選擇:
使用「繼承TinkerApplication + DefaultApplicationLike」。
使用「DefaultLifeCycle註解 + DefaultApplicationLike」。
當然,如果你覺得你自定義的Application不會用到熱修復,可無視這部分;
但下方程式碼中的initTinker()方法記得要拷貝到你專案中,用於初始化Tinker。
第1種方式感覺比較雞肋,這裡使用第2種(Tinker官方推薦的方式):「DefaultLifeCycle註解 + TinkerApplicationLike」,DefaultLifeCycle註解生成Application,下面就來編寫Application的代理類:
1、編寫TinkerApplicationLike
將下方的程式碼拷貝到專案中,註釋簡單明瞭,不多解釋:
@SuppressWarnings("unused")
@DefaultLifeCycle(application = "com.lqr.tinker.MyApplication",// application類名。只能用字串,這個MyApplication檔案是不存在的,但可以在AndroidManifest.xml的application標籤上使用(name)
flags = ShareConstants.TINKER_ENABLE_ALL,// tinkerFlags
loaderClass = "com.tencent.tinker.loader.TinkerLoader",//loaderClassName, 我們這裡使用預設即可!(可不寫)
loadVerifyFlag = false)//tinkerLoadVerifyFlag
public class TinkerApplicationLike extends DefaultApplicationLike {
private Application mApplication;
private Context mContext;
private Tinker mTinker;
// 固定寫法
public TinkerApplicationLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
// 固定寫法
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallbacks(Application.ActivityLifecycleCallbacks callback) {
getApplication().registerActivityLifecycleCallbacks(callback);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
mApplication = getApplication();
mContext = getApplication();
initTinker(base);
// 可以將之前自定義的Application中onCreate()方法所執行的操作搬到這裡...
}
private void initTinker(Context base) {
// tinker需要你開啟MultiDex
MultiDex.install(base);
TinkerManager.setTinkerApplicationLike(this);
// 設定全域性異常捕獲
TinkerManager.initFastCrashProtect();
//開啟升級重試功能(在安裝Tinker之前設定)
TinkerManager.setUpgradeRetryEnable(true);
//設定Tinker日誌輸出類
TinkerInstaller.setLogIml(new MyLogImp());
//安裝Tinker(在載入完multiDex之後,否則你需要將com.tencent.tinker.**手動放到main dex中)
TinkerManager.installTinker(this);
mTinker = Tinker.with(getApplication());
}
}
2、搬運自定義Application中的操作
把專案中在自定義Application的操作移到TinkerApplicationLike的onCreate()或onBaseContextAttached()方法中。
public class TinkerApplicationLike extends DefaultApplicationLike {
...
@Override
public void onCreate() {
super.onCreate();
// 將之前自定義的Application中onCreate()方法所執行的操作搬到這裡...
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
mApplication = getApplication();
mContext = getApplication();
initTinker(base);
// 或搬到這裡...
}
}
3、清單檔案中註冊
將@DefaultLifeCycle中application對應的值,即"com.lqr.tinker.MyApplication",賦值給清單檔案的application標籤的name屬性,如下:
<application
android:name="com.lqr.tinker.MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
</application>
注意:
此時name屬性會報紅,因為專案原始碼中根本不存在MyApplication.java檔案,但不必擔心,因為它是動態生成的,Build一下專案就好了,不管它也無所謂。
對於Application代理類的詳細說明,請參考:「Tinker官方Wiki:Application代理類」。
常用 API
到這裡就已經整合好Tinker了,但只是本地整合而已,下面就來進行補丁包的製作,並讓心愛的app打上補丁吧。
服務端下發補丁包到app的文章之後會陸續釋出更新。
現在來了解下程式碼中會用到的幾個Tinker的重要API。
1、請求打補丁
TinkerInstaller.onReceiveUpgradePatch(context, 補丁包的本地路徑);
2、解除安裝補丁
Tinker.with(getApplicationContext()).cleanPatch();// 解除安裝所有的補丁
Tinker.with(getApplicationContext()).cleanPatchByVersion(版本號)// 解除安裝指定版本的補丁
3、殺死應用的其他程序
ShareTinkerInternals.killAllOtherProcess(getApplicationContext());
4、Hack方式修復so
TinkerLoadLibrary.installNavitveLibraryABI(this, abi);
abi:cpu架構型別
5、非Hack方式修復so
TinkerLoadLibrary.loadLibraryFromTinker(getApplicationContext(), "lib/" + abi, so庫的模組名); // 載入任意abi庫
TinkerLoadLibrary.loadArmLibrary(getApplicationContext(), so庫的模組名); // 只適用於載入armeabi庫
TinkerLoadLibrary.loadArmV7Library(getApplicationContext(), so庫的模組名); // 只適用於載入armeabi-v7a庫
loadArmLibrary()與loadArmV7Library()本質是呼叫了loadLibraryFromTinker(),有興趣的可以檢視下原始碼。
對於Tinker所有API的詳細說明,請參考:「Tinker官方Wiki:Tinker-API概覽」。
測試
因為佈局簡單且不是重點,這裡就給出一張Demo的執行圖片,剩下的就靠想像了。
...
字數有限制,測試剩餘內容和下面兩大部分請點選閱讀原文檢視
打包
其他
github 地址
https://github.com/GitLqr/HotFixDemo
Demo中的Module說明:
app:熱修復原理Demo
tinker-local:本地整合Tinker熱修復Demo
jnitest:生成簡單so檔案的Demo
與之相關