1. 程式人生 > >熱修復——Tinker 的整合與使用

熱修復——Tinker 的整合與使用

?wxfrom=5&wx_lazy=1

作者 | CSDN_LQR

地址 | https://juejin.im/post/5a27bdaf6fb9a044fa19bcfc

宣告 | 本文是 CSDN_LQR 原創,已獲授權釋出,未經原作者允許請勿轉載

簡述

Tinker是微信官方的Android熱補丁解決方案,它支援動態下發程式碼、So庫以及資源,讓應用能夠在不需要重新安裝的情況下實現更新。當然,你也可以使用Tinker來更新你的外掛。

上面是Tinker官方Wiki的原話,意思嘛相信大家都看得明白,但注意啦,它並沒有說Tinker可以讓補丁實時生效(也叫無感知更新),它必須在打上補丁後重啟App(重啟程序),補丁才會發揮作用,這跟阿里的熱修復方案有著本質的區別。在開始整合Tinker之前,我們有必要了解清楚,Tinker有那些不足,下面是Tinker的已知問題:

  1. Tinker不支援修改AndroidManifest.xml,Tinker不支援新增四大元件(1.9.0支援新增非export的Activity);

  2. 由於Google Play的開發者條款限制,不建議在GP渠道動態更新程式碼;

  3. 在Android N上,補丁對應用啟動時間有輕微的影響;

  4. 不支援部分三星android-21機型,載入補丁時會主動丟擲"TinkerRuntimeException:checkDexInstall failed";

  5. 對於資源替換,不支援修改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無法動態修復,所以有兩種選擇:

  1. 使用「繼承TinkerApplication + DefaultApplicationLike」。

  2. 使用「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說明:

  1. app:熱修復原理Demo

  2. tinker-local:本地整合Tinker熱修復Demo

  3. jnitest:生成簡單so檔案的Demo

?

與之相關

?