在上一篇《熱修復——Tinker的集成與使用》中,根據Tinker官方Wiki集成了Tinker,但那僅僅只是本地集成,有一個重要的問題沒有解決,那就是補丁從服務器下發到用戶手機上,若是你團隊中的後臺開發人員實力夠強,那麼徹底能夠本身作一個補丁管理系統,但我想應該沒多少人願意花精力在這個後臺管理系統的開發上面吧,且開發有時候就是在造bug,鬼知道會挖出一個多大的坑呢?對於這樣的一個問題,據我所知,市面上有3種Tinker的補丁管理系統,以下:android
「Bugly」和「tinker-manager」是免費的,「tinkerpatch」是收費的,由於「tinkerpatch」收費,因此暫時不作考慮。Bugly由騰訊團隊開發並維護,穩定性確定沒得說,而「tinker-manager」是GitHub上我的開發者開發維護的,穩定性無法保證(我沒有貶低開發者的意思,畢竟勢單力薄,人多力量大嘛),故本人以爲,Bugly是目前最優的Tinker熱修復解決方案。在開始進入Bugly集成以前,你能夠先點擊下載Demo試試看看。git
要使用Bugly的熱修復功能,首先得註冊並登陸Bugly,而後點擊進入「Bugly產品頁面」,或點擊「個人產品 」。github
我這個帳號以前是沒有建立過產品,因此這裏什麼也沒有,接着點擊「新建產品」。api
填寫必要的信息後,點擊「保存」。服務器
經過「產品設置」,選擇剛剛建立的產品(圖中第3步),能夠查看到產品對應的App ID。app
這個App ID很重要,先記錄好,後續會用到。框架
Demo的App ID爲: 3062edb401。不要用個人,對你來講一點用處都沒有,請使用你本身產品的App ID。ide
項目的build.gradle:函數
dependencies {
classpath 'com.android.tools.build:gradle:3.0.0'
// tinkersupport插件(1.0.3以上無須再配置tinker插件)
classpath "com.tencent.bugly:tinker-support:1.1.1"
}
複製代碼
app的build.gradle:工具
apply from: 'tinker-support.gradle'
android {
defaultConfig {
...
// 開啓multidex
multiDexEnabled true
}
// recommend
dexOptions {
jumboMode = true
}
// 簽名配置
signingConfigs {
release {
try {
storeFile file("./keystore/release.keystore")
storePassword "testres"
keyAlias "testres"
keyPassword "testres"
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
debug {
storeFile file("./keystore/debug.keystore")
}
}
// 構建類型
buildTypes {
release {
minifyEnabled true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
debuggable true
minifyEnabled false
signingConfig signingConfigs.debug
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
dependencies {
...
implementation "com.android.support:multidex:1.0.1" // 多dex配置
implementation 'com.tencent.bugly:crashreport_upgrade:1.3.4'// 遠程倉庫集成方式(推薦)
}
複製代碼
簽名配置部分請根據你項目的實際狀況修改,如:
在app的build.gradle文件同級目錄下建立一個tinker-support.gradle文件,內容以下:
apply plugin: 'com.tencent.bugly.tinker-support'
def bakPath = file("${buildDir}/bakApk/")
/**
* 此處填寫每次構建生成的基準包目錄
*/
def baseApkDir = "tinker-bugly-1211-16-01-34"
def myTinkerId = "base-" + rootProject.ext.android.versionName // 用於生成基準包(不用修改)
//def myTinkerId = "patch-" + rootProject.ext.android.versionName + ".0.0" // 用於生成補丁包(每次生成補丁包都要修改一次,最好是 patch-${versionName}.x.x)
/**
* 對於插件各參數的詳細解析請參考
*/
tinkerSupport {
// 開啓tinker-support插件,默認值true
enable = true
// 是否啓用加固模式,默認爲false.(tinker-spport 1.0.7起支持)
// isProtectedApp = true
// 是否開啓反射Application模式
enableProxyApplication = true
// 是否支持新增非export的Activity(注意:設置爲true才能修改AndroidManifest文件)
supportHotplugComponent = true
// 指定歸檔目錄,默認值當前module的子目錄tinker
autoBackupApkDir = "${bakPath}"
// 是否啓用覆蓋tinkerPatch配置功能,默認值false
// 開啓後tinkerPatch配置不生效,即無需添加tinkerPatch
overrideTinkerPatchConfiguration = true
// 編譯補丁包時,必需指定基線版本的apk,默認值爲空
// 若是爲空,則表示不是進行補丁包的編譯
// @{link tinkerPatch.oldApk }
baseApk = "${bakPath}/${baseApkDir}/app-release.apk"
// 對應tinker插件applyMapping
baseApkProguardMapping = "${bakPath}/${baseApkDir}/app-release-mapping.txt"
// 對應tinker插件applyResourceMapping
baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-release-R.txt"
// 構建基準包和補丁包都要指定不一樣的tinkerId,而且必須保證惟一性
tinkerId = "${myTinkerId}"
// 構建多渠道補丁時使用
// buildAllFlavorsDir = "${bakPath}/${baseApkDir}"
}
/**
* 通常來講,咱們無需對下面的參數作任何的修改
* 對於各參數的詳細介紹請參考:
* https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
*/
tinkerPatch {
...
}
複製代碼
當overrideTinkerPatchConfiguration = true時,tinkerPatch能夠省略不寫,Bugly會加載默認的Tinker配置。但請注意,若是你的so文件不是存放在libs目錄下(與src目錄同級),又或者資源文件的存放在你自定義的目錄中,那麼這時你要當心了,這些文件在製做補丁包時不會被檢測,也就是說這些so文件和資源文件將不會被熱修復,這種狀況下就須要將overrideTinkerPatchConfiguration = false,並設置tinkerPatch的lib和res屬性。
其它具體的配置與說明能夠查看「Tinker-接入指南」。
baseApkDir是基準包(也稱基線包)的目錄,在生產補丁時須要根據基準包在bakApk下具體文件夾名字修改,如:bakApk/xxxx,到時生成補丁包時要將baseApkDir的值改成xxxx。(xxxx是Tinker自動生成的,根據時間戳來命名)。
tinkerId是Bugly熱修復方案最最重要的一個因素,通常取值爲git版本號、versionName等等(我習慣用versionName),它會將補丁包與基準包產生對應關係,假設基準包的tinkerId爲 base-1.0,則生成的補丁包中的YAPATCH.MF文件關係以下:
Bugly要求baseApk(基準包)的tinkerId與補丁包的tinkerId要不同。因此,在生成基準包時,請用以下tinkerId:
def myTinkerId = "base-" + rootProject.ext.android.versionName // 用於生成基準包(不用修改)
複製代碼
當生成補丁包時,請使用以下tinkerId:
def myTinkerId = "patch-" + rootProject.ext.android.versionName + ".0.0" // 用於生成補丁包(每次生成補丁包都要修改一次,最好是 patch-${versionName}.x.x)
複製代碼
對於同一個基準包,咱們可能會屢次生成補丁包上傳到Bugly的熱修復管理後臺,這時,這些補丁包的tinkerId也要不同,否則的話,當客戶手機上的App在獲取補丁時,會錯亂(親測,當同個基準包的補丁包的tinkerId同樣時,App每次重啓都會獲取不一樣的補丁包,致使tinkerId相同的補丁包輪流下發)。因此,"patch-" + rootProject.ext.android.versionName + ".0.0"中的".0.0"(稱爲計數)就是爲了區分每次生成的補丁包,如.0.1,.0.2等等,建議versionName更新時計數重置。
由於Tinker的配置放在了tinker-support.gradle文件中,與app的build.gradle不在同一個文件中,因此沒辦法經過android.defaultConfig.versionName直接獲取App的versionName,這裏我使用了config.gradle來提取共同的屬性,rootProject.ext.android.versionName獲取的是config.gradle中的versionName屬性,詳情請百度。
def myTinkerId = "patch-" + rootProject.ext.android.versionName + ".0.0" // 用於生成補丁包(每次生成補丁包都要修改一次,最好是 patch-${versionName}.x.x)
複製代碼
對於一個基準包,能夠在Bugly上發佈多個補丁包(切記tinkerid不一樣),這裏或許會讓你誤覺得計數越大,代表補丁越新,這是錯誤的,這個計數僅僅只是區分不一樣的補丁包而已,它沒有標記補丁新舊的做用,補丁新舊由Bugly來斷定,最後上傳的補丁即是最新的補丁,舉個例子,我在昨天上傳了tinkerid爲"patch-1.0.0.9"的補丁1,在今天上傳了tinkerid爲"patch-1.0.0.1"的補丁2,雖然補丁2的計數比補丁1小,但補丁2比補丁1晚上傳,因此補丁2是最新的補丁,即補丁新舊與計數無關。Bugly會下發並應用最新的補丁(即補丁2),但仍是建議計數從小到大計算,這裏僅僅只是說明Bugly如何斷定補丁新舊罷了。
Bugly的初始化工做須要在Application中完成,但對原生Tinker來講,默認的Application是沒法實現熱修復的。看過Tinker官方Wiki的人應該知道,Tinker針對Application沒法熱修復的問題,給予開發者兩個選擇,分別是:
這2種選擇都須要對自定義的Application進行改造,對於自定義Application代碼很少的狀況來講還能夠接受,但有些狀況仍是比較"討厭"這2種選擇的,對此,Bugly給出了它的2種解決方法,分別以下:
DefaultLifeCycle註解在Bugly中被閹割了。
分別對應tinker-support.gradle文件中enableProxyApplication的值:true或false。
Bugly將經過反射的方式針對項目中自定義的Application動態生成新的Application,下圖是源碼中的AndroidManifest.xml和編譯好的apk中的AndroidManifest.xml:
既然將enableProxyApplication的值設置爲true,那接下來的重點就是完成Bugly的初始化工做了。須要在自定義的Application的onCreate()中進行Bugly的配置,在attachBaseContext()中進行Bugly的安裝:
public class MyApplication extends Application {
private Context mContext;
@Override
public void onCreate() {
super.onCreate();
mContext = getApplicationContext();
// 這裏實現SDK初始化,appId替換成你的在Bugly平臺申請的appId
// 調試時,將第三個參數改成true
configTinker();
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
// you must install multiDex whatever tinker is installed!
MultiDex.install(mContext);
// 安裝tinker
// 此接口僅用於反射Application方式接入。
Beta.installTinker();
}
}
複製代碼
注意:
- Bugly的安裝必須在attachBaseContext()方法中,不然將沒法從Bugly服務器獲取最新補丁。
- tinker須要你開啓MultiDex,你須要在dependencies中進行配置compile "com.android.support:multidex:1.0.1"纔可使用MultiDex.install方法。
最後在清單文件中,聲明使用咱們自定義的Application便可:
<application
android:name="com.lqr.MyApplication"
...>
複製代碼
這是Bugly推薦的方式,穩定性有保障(由於第1種方式使用的是反射,可能會存在不穩定的因素),它須要對Application進行改造,首先就是繼承TinkerApplication,而後在默認的構造函數中,將第2個參數修改成你項目中的ApplicationLike繼承類的全限定名稱:
public class SampleApplication extends TinkerApplication {
public SampleApplication() {
super(ShareConstants.TINKER_ENABLE_ALL, "com.lqr.SampleApplicationLike",
"com.tencent.tinker.loader.TinkerLoader", false);
}
}
複製代碼
注意:這個類集成TinkerApplication類,這裏面不作任何操做,全部Application的代碼都會放到ApplicationLike繼承類當中
參數解析
參數1:tinkerFlags 表示Tinker支持的類型 dex only、library only or all suuport,default: TINKER_ENABLE_ALL
參數2:delegateClassName Application代理類 這裏填寫你自定義的ApplicationLike
參數3:loaderClassName Tinker的加載器,使用默認便可
參數4:tinkerLoadVerifyFlag 加載dex或者lib是否驗證md5,默認爲false
接着就是建立ApplicationLike繼承類:
public class SampleApplicationLike extends DefaultApplicationLike {
public static final String TAG = "Tinker.SampleApplicationLike";
private Application mContext;
public SampleApplicationLike(Application application, int tinkerFlags,
boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime,
long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
@Override
public void onCreate() {
super.onCreate();
mContext = getApplication();
configTinker();
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
// you must install multiDex whatever tinker is installed!
MultiDex.install(base);
// 安裝tinker
Beta.installTinker(this);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks) {
getApplication().registerActivityLifecycleCallbacks(callbacks);
}
@Override
public void onTerminate() {
super.onTerminate();
Beta.unInit();
}
}
複製代碼
注意:
SampleApplicationLike這個類是Application的代理類,之前全部在Application的實現必需要所有拷貝到這裏,在onCreate方法調用SDK的初始化方法,在onBaseContextAttached中調用Beta.installTinker(this)。
最後在清單文件中,聲明改造好的Application(注意不是ApplicationLike):
<application
android:name="com.lqr.SampleApplication"
...>
複製代碼
這是Bugly官方給出的配置,應有盡有,註釋也很nice,請仔細看看,對項目的功能拓展與用戶體驗有幫助:
private void configTinker() {
// 設置是否開啓熱更新能力,默認爲true
Beta.enableHotfix = true;
// 設置是否自動下載補丁,默認爲true
Beta.canAutoDownloadPatch = true;
// 設置是否自動合成補丁,默認爲true
Beta.canAutoPatch = true;
// 設置是否提示用戶重啓,默認爲false
Beta.canNotifyUserRestart = true;
// 補丁回調接口
Beta.betaPatchListener = new BetaPatchListener() {
@Override
public void onPatchReceived(String patchFile) {
Toast.makeText(mContext, "補丁下載地址" + patchFile, Toast.LENGTH_SHORT).show();
}
@Override
public void onDownloadReceived(long savedLength, long totalLength) {
Toast.makeText(mContext,
String.format(Locale.getDefault(), "%s %d%%",
Beta.strNotificationDownloading,
(int) (totalLength == 0 ? 0 : savedLength * 100 / totalLength)),
Toast.LENGTH_SHORT).show();
}
@Override
public void onDownloadSuccess(String msg) {
Toast.makeText(mContext, "補丁下載成功", Toast.LENGTH_SHORT).show();
}
@Override
public void onDownloadFailure(String msg) {
Toast.makeText(mContext, "補丁下載失敗", Toast.LENGTH_SHORT).show();
}
@Override
public void onApplySuccess(String msg) {
Toast.makeText(mContext, "補丁應用成功", Toast.LENGTH_SHORT).show();
}
@Override
public void onApplyFailure(String msg) {
Toast.makeText(mContext, "補丁應用失敗", Toast.LENGTH_SHORT).show();
}
@Override
public void onPatchRollback() {
}
};
// 設置開發設備,默認爲false,上傳補丁若是下發範圍指定爲「開發設備」,須要調用此接口來標識開發設備
Bugly.setIsDevelopmentDevice(mContext, false);
// 多渠道需求塞入
// String channel = WalleChannelReader.getChannel(getApplication());
// Bugly.setAppChannel(getApplication(), channel);
// 這裏實現SDK初始化,appId替換成你的在Bugly平臺申請的appId
Bugly.init(mContext, "e9d0b7f57f", true);
}
複製代碼
這裏就用到了一開始獲取到的App ID了,將其傳入Bugly.init()方法的第二個參數,切記,用你本身的App ID。
其中以下兩個方法很重要:
設置當前設備是否是開發設備,這跟Bugly上傳補丁包時所選的"下發範圍"有關。
這個方法除了設置App ID外,還能夠設置是否輸出Log,能夠觀察到Bugly在App啓動時作了哪些聯網操做。
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.READ_LOGS"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
複製代碼
<activity
android:name="com.tencent.bugly.beta.ui.BetaActivity"
android:configChanges="keyboardHidden|orientation|screenSize|locale"
android:theme="@android:style/Theme.Translucent"/>
複製代碼
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
複製代碼
若是你使用的第三方庫也配置了一樣的FileProvider, 能夠經過繼承FileProvider類來解決合併衝突的問題,示例以下:
<provider
android:name=".utils.BuglyFileProvider"
android:authorities="${applicationId}.fileProvider"
android:exported="false"
android:grantUriPermissions="true"
tools:replace="name,authorities,exported,grantUriPermissions">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"
tools:replace="name,resource"/>
</provider>
複製代碼
在res目錄新建xml文件夾,建立provider_paths.xml文件以下:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!-- /storage/emulated/0/Download/${applicationId}/.beta/apk-->
<external-path name="beta_external_path" path="Download/"/>
<!--/storage/emulated/0/Android/data/${applicationId}/files/apk/-->
<external-path name="beta_external_files_path" path="Android/data/"/>
</paths>
複製代碼
注:1.3.1及以上版本,能夠不用進行以上配置,aar已經在AndroidManifest配置了,而且包含了對應的資源文件。
# Bugly混淆規則
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
# 避免影響升級功能,須要keep住support包的類
-keep class android.support.**{*;}
複製代碼
好了,集成完畢,接下來就是製做基準包、補丁包和上傳補丁包了。
在app編碼完成並測試完成後,就是打包上線了,上線前打的包就是基準包啦,下面咱們就來製做基準包,分3步:
一般主Module的名字是"app",但我這個Demo是"tinker-bugly",因此你執行第3步時,要根據具體項目找到要製做基準包的主Module。
AS在執行assembleRelease指令時,就是在編譯基準包了,當編譯完成時,app的build目錄下會自動生成基準包文件夾,以時間戳來命名的(也就是說,每次執行assembleRelease指令都會在build目錄建立不一樣的基準包文件夾)。
這3個文件對以後製做補丁包來講是至關重要的,你須要作的就是將這3個文件保存好,能夠保存到雲盤、Git服務器上等等,但就不要讓它就這麼放着,由於在你執行clean Project時,app的build目錄會被刪除,這樣基準包及mapping與R文件都會丟失。
到這裏,你就能夠把它(基準包:tinker-bugly-release.apk)上架到應用市場了。試下Demo:
本篇不涉及具體的加固與多渠道打包。
若是你的app須要加固,那就須要在製做基準包以前,將tinker-support.gradle文件的isProtectedApp = true的註釋去掉,而後加固,從新簽名,最後上架,它對加固平臺也有必定的要求。
詳情見「Bugly熱更新使用範例文檔最後:加固打包」部分。
分「gradle配置productFlavors方式」與「多渠道打包工具打多渠道包方式(推薦)」。
詳情見「Bugly熱更新使用範例文檔:多渠道打包」部分。
如今要動態修復App了,對於代碼修復、so庫修復、資源文件修復,分別對應Demo中的"say something"、"get string from .so"、"個人頭像",修復過程無非是改代碼,替換so文件,替換資源文件,這裏就不演示了,直接開始製做補丁包,先將tinker-support.gradle文件打開。
確保基準包及相關文件的命名與配置文件中的一致:
打開側邊的Gradle標籤,找到項目的主Module,雙擊tinker-support下的buildTinkerPatchRelease指令,生成補丁包。
當編譯完成後,在app的build/outputs/patch目錄下會在"patch_singed_7zip.apk"文件,它就是補丁包,雙擊打開它,能夠看到其中有一個YAPATCH.MF,裏面記錄了基準包與補丁包的tinkerId(二者是確定不一樣,若是同樣則說明配置有問題了)。
首先,點擊進入「Bugly產品頁面」,或點擊「個人產品 」查看個人產品。
點擊你要管理的產品後,依次點擊"應用升級"、"熱更新",能夠查看到該產品的補丁下發狀況(這個產品我還沒上傳過補丁,故一片空白)。
按下圖順序操做便可上傳補丁包:
有可能你在上傳完補丁包時,頁面會提示"未匹配到可應用補丁包的App版本,請確認補丁包的基線版本是否已經發布"。
遇到這種狀況請先冷靜,首先來講明一件事:Bugly怎麼知道基線版本是否已經發布?
一般按咱們理解的,基準包發佈就是上架到應用市場,但應用市場又不會通知Bugly某某產品已經上架了,對吧。其實,Bugly的上架通知是這樣的:當基準包在手機上啓動時,Bugly框架就會讓App聯網通知Bugly的服務器,同時上傳當前App的版本號、tinkerId等信息,它這麼作的目的有以下兩個:
因此,當出現了"未匹配到可應用補丁包的App版本,請確認補丁包的基線版本是否已經發布"這樣的提示時,能夠肯定,這個基準包的tinkerId等信息沒有被上傳到Bugly服務器,對此,鄙人將踩過的坑總結起來,摸索出了本身的解決方法,分以下幾步:
像我就犯過這樣的錯,明明在tinker-support.gradle文件中設置了enableProxyApplication = true,結果在AndroidManifest.xml中卻聲明瞭TinkerApplication的繼承類。
因此這裏只須要將AndroidManifest.xml中聲明咱們自定義的Application便可(MyApplication)。
除了聯網問題之外,其餘的幾種狀況都須要從新生成基準包。這裏再分享一個能夠快速肯定App是否有上傳過版本信息的方法:
先驗證下上面的方法,當我把問題解決掉以後,把從新生成的基準包安裝到手機上打開(此時Bugly框架會上傳App的版本號、tinkerId到服務器),再查看"版本管理",出現了,版本號爲"1.0"(其實就是App的versionName)。
再回頭來看看上傳補丁,此次又會有什麼不一樣呢?
耶,成功。點擊"當即下發",能夠看到如今補丁處於"下發中"狀態:
隨便來看看用戶手中的App是什麼反應吧(真正將補丁下發到用戶手機上的這段時間可能會有點久,不是當即下發的):
再回頭看看Bugly服務器上的補丁下發狀況:
Bugly服務器除了能夠上傳下發補丁外,還能夠對補丁進行管理: