Android熱更新Tinker + 多渠道打包 + 加固的流程詳解
一、Tinker熱修復
關於熱修復的作用,不用多說了,一句話概括就是通過讓使用者無感的方式來修復線上應用的bug。這裡介紹的是微信Tinker。
下面的接入方式都是參考自Tinker官方文件來。我這裡主要是把我接入的步驟(通過AndroidStudio + gradle方式)說一遍。下面的步驟都是基於tinker-support外掛:1.0.8版本,以及sdk 1.3.1 進行。所以檢視此文時需要參考官方文件是否有更新,避免接入的方式改變導致接入出現問題。Tinker的官方接入指南地址為:https://bugly.qq.com/docs/user-guide/instruction-manual-android-hotfix/?v=20170912151050
1、新增外掛依賴
工程根目錄下“build.gradle”檔案中新增:
buildscript {
repositories {
jcenter()
}
dependencies {
// tinkersupport外掛, 其中lastest.release指拉取最新版本,也可以指定明確版本號,例如1.0.4
classpath "com.tencent.bugly:tinker-support:1.0.8"
}
}
2、整合SDK
gradle配置:
在app module的“build.gradle”檔案中新增(示例配置):
android {
defaultConfig {
ndk {
//設定支援的SO庫架構
abiFilters 'armeabi' //, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a'
}
}
}
dependencies {
compile "com.android.support:multidex:1.0.1" // 多dex配置
//註釋掉原有bugly的倉庫
//compile 'com.tencent.bugly:crashreport:latest.release'//其中latest.release指代最新版本號,也可以指定明確的版本號,例如2.3.2
compile 'com.tencent.bugly:crashreport_upgrade:1.3.1'
compile 'com.tencent.bugly:nativecrashreport:latest.release' //其中latest.release指代最新版本號,也可以指定明確的版本號,例如2.2.0
}
後續更新升級SDK時,只需變更配置指令碼中的版本號即可。
在app module的“build.gradle”檔案中新增:
// 依賴外掛指令碼,平時編寫程式碼的時候這裡其實可以註釋,不然每次build都會生成基準包,只要在打正式包和補丁包的時候放開註釋就好了
apply from: 'tinker-support.gradle'
您需要在同級目錄下建立tinker-support.gradle這個檔案哦。
tinker-support.gradle內容如下所示(示例配置):
apply plugin: 'com.tencent.bugly.tinker-support'
def bakPath = file("${buildDir}/bakApk/")
/**
* 此處填寫每次構建生成的基準包目錄
*/
def baseApkDir = "app-0208-15-10-00"
/**
* 對於外掛各引數的詳細解析請參考
*/
tinkerSupport {
// 開啟tinker-support外掛,預設值true
enable = 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 = "base-1.0.1"
// 構建多渠道補丁時使用
// buildAllFlavorsDir = "${bakPath}/${baseApkDir}"
// 是否啟用加固模式,預設為false.(tinker-spport 1.0.7起支援)
// isProtectedApp = true
// 是否開啟反射Application模式
enableProxyApplication = false
}
/**
* 一般來說,我們無需對下面的引數做任何的修改
* 對於各引數的詳細介紹請參考:
* https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
*/
tinkerPatch {
//oldApk ="${bakPath}/${appName}/app-release.apk"
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
//tinkerId = "1.0.1-base"
//applyMapping = "${bakPath}/${appName}/app-release-mapping.txt" // 可選,設定mapping檔案,建議保持舊apk的proguard混淆方式
//applyResourceMapping = "${bakPath}/${appName}/app-release-R.txt" // 可選,設定R.txt檔案,通過舊apk檔案保持ResId的分配
}
}
注意,這裡記得修改為基線版本包名稱的修改。我這裡的基線版本輸出檔名為 :tinker_demo-release
所以修改為如下:
3、初始化SDK
我這裡使用的是enableProxyApplication = false 的情況 。因為這是Tinker推薦的接入方式,一定程度上會增加接入成本,但具有更好的相容性。true的情況具體看官方文件。
整合Bugly升級SDK之後,我們需要按照以下方式自定義ApplicationLike來實現Application的程式碼(以下是示例):
自定義Application:
public class SampleApplication extends TinkerApplication {
public SampleApplication() {
super(ShareConstants.TINKER_ENABLE_ALL, "xxx.xxx.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
然後記得在manifest檔案中修改為自定義的Application:
自定義ApplicationLike,我們只需把下面的程式碼拷貝到我們的專案中就可以了,記得修改onCreate方法中的Bugly.init()中的key:
public class SampleApplicationLike extends DefaultApplicationLike {
public static final String TAG = "Tinker.SampleApplicationLike";
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();
// 這裡實現SDK初始化,appId替換成你的在Bugly平臺申請的appId
// 除錯時,將第三個引數改為true
Bugly.init(getApplication(), "0123456789", false);
}
@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
// TinkerManager.installTinker(this); 替換成下面Bugly提供的方法
Beta.installTinker(this);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void registerActivityLifecycleCallback(Application.ActivityLifecycleCallbacks callbacks) {
getApplication().registerActivityLifecycleCallbacks(callbacks);
}
}
注意:tinker需要你開啟MultiDex,你需要在dependencies中進行配置compile “com.android.support:multidex:1.0.1”才可以使用MultiDex.install方法; SampleApplicationLike這個類是Application的代理類,以前所有在Application的實現必須要全部拷貝到這裡,在onCreate方法呼叫SDK的初始化方法,在onBaseContextAttached中呼叫Beta.installTinker(this);。
在此,Tinker的接入已經完成,是不是比想象中簡單多了簡單。
4、使用
打正式包:
首先打正式包前,記得修改tinker-support.gradle檔案中的tinkerId:
tinkerId最好是一個唯一標識,例如git版本號、versionName等等。 如果你要測試熱更新,你需要對基線版本進行聯網上報。
這裡強調一下,基線版本配置一個唯一的tinkerId,而這個基線版本能夠應用補丁的前提是整合過熱更新SDK,並啟動上報過聯網,這樣我們後臺會將這個tinkerId對應到一個目標版本,例如tinkerId = “bugly_1.0.0” 對應了一個目標版本是1.0.0,基於這個版本打的補丁包就能匹配到目標版本。
然後通過gradle進行打包:
然後會在tinkerSupport.gradle上配置的路徑輸出基線包,我這裡是:build檔案下的baseApk下:
在此我們需要記得儲存好基線包資料夾以及基線包的TinkerId(補丁包與根據基線包TinkerId進行匹配,所以TinkerId一般使用版本號來標識)。
好了,我們把這基線包釋出到bugly平臺上即可:
打補丁包:
那現在剛剛釋出線上的應用有問題了,那我們首先在基線包的程式碼上進行程式碼的修復,然後進行打補丁包進行修復了。
首先,修改tinkersuppor.gradle裡面的baseApkDir配置,就是上面打基線包生成的以時間戳命名的資料夾名稱:
然後再修改tinkerId:
然後通過gradle進行打補丁包:
最後會在配置的指定檔案下生成補丁包檔案:
然後我們把上面的 patch_signed_7zip.apk檔案上傳到bugly分發即可:
連線手機除錯檢視log,如果列印以下log證明補丁包已經下載成功:
列印以下log證明 補丁包已經合併到手機的應用中:
至此bug修復完成。
5、問題:
1)、有人在寫Demo測試時,把基準包上傳了,補丁包上傳時出了未匹配到可應用的App版本的報錯。
那是因為我們的基準包雖然已經上傳了,但是還沒有一部手機安裝使用並上報聯網。所以Bugly的後臺伺服器找不到對應要下發的App版本,所以導致了該錯誤。
解決方案是,用手機安裝一次基準包,檢視log有以下列印,證明聯網上報成功,然後在重新發布一下補丁包就不會出現以上問題了。
2 )、怎樣去測試我這個補丁包能否正常使用,而且先不下發給所有使用者?
那這時就要使用開發裝置進行測試了。
我們在釋出新補丁時有個開發裝置可以選擇,我們可以通過Bugly.setIsDevelopmentDevice(getApplication(), true);方法設定為開發者裝置吶?而且需要在打基準包時就需要設定好。
那這時會有人疑問要去修改線上的基準包?這不實際吧,而且也做不了啊。
那怎麼辦呢?
首先 我們在線上版本的程式碼(每次上線前要利用git去保留一個線上版本的分支)上新增設定為開發者裝置的程式碼,然後重新進行打基準包的操作(記得: tinkerId 和線上版本的一致),把這個基準包安裝到測試手機上(注意不是上傳到bugly)。
然後我們再進行程式碼的修復,打補丁包。把補丁包上傳到bugly,設定為開發裝置。然後我們在重新退出再開啟測試機上的應用,檢視日誌,列印如下表明補丁包已經分發合併到當前測試手機的應用上。而線上的使用者是接收不了的。
最後我們已經確保這補丁包沒問題了,我們要把這補丁包分發給所有使用者。那我們就可以再bugly平臺上重新編輯補丁包的下發設定為全部裝置即可。
到此,我們的Tinker熱更新接入和使用已經完成。
二、 多渠道打包 + 加固。
關於為什麼要進行多渠道打包的問題,相信大家都清除,就是為了方便統計更清楚地和了解應用在各應用市場的分佈情況,便於產品和運營做一些針對性的產品運營推廣方案。
Android原生是提供了一套多渠道打包的方案給開發者的,然後也有很多其它第三方也提供了打渠道包的方案,比如熱門的美團walle打包,360打包工具等。
那我們就針對這幾種打包方案結合Tinker熱修復的接入進行分析:
1、Android原生的多渠道打包:
Android原生的多渠道打包是通過再gradle檔案中新增productFloavors配置,實現大渠道打包的。
但是這種方案有很多缺點。首先就是它打包速度很慢,每打一個包都要重新編譯一次。假如我打一個包要3分鐘,那我打10個渠道包,就要30分鐘。假如中間有一些問題又需要重新進行打包,那不就是一個坑爹的過程?
而另一方面,因為這種方式,會修改buildConfig類中的FLAVOR欄位, 會使得我們的每個安裝包的dex檔案都是不一樣的,那使用Tinker熱修復打的補丁包就要針對具體的渠道進行打補丁。假如我有10個渠道包,那就要針對這10個渠道包打10個補丁包,那是一個更坑爹的過程,老闆高薪請你來打包的,內心慚愧啊。那就算你耐得住打包這寂寞過程,buly也是隻允許同時下發5個版本的補丁。所以這個方式是不實際的。
美團的walle打包方案是基於Android Signature V2 Schme 簽名下的新一代渠道包打包神器,它通過在Apk中的APK SignNature Block 區塊新增自定義的渠道資訊來生成渠道包,從而提高了渠道包的生成效率。也就是說它只是編譯打包一次,然後每個渠道複製一次再往裡面新增渠道資訊而已。所以這個過程就算打100個包,速度也是非常快的。
walle有兩種整合方式,一種是通過gradle進行整合,一種是通過命令列工具使用方式(https://github.com/Meituan-Dianping/walle/blob/master/walle-cli/README.md)
這裡介紹gradle整合方式:
配置build.gradle
在位於專案的根目錄 build.gradle 檔案中新增Walle Gradle外掛的依賴, 如下:
buildscript {
dependencies {
classpath 'com.meituan.android.walle:plugin:1.1.6'
}
}
並在當前App的 build.gradle 檔案中apply這個外掛,並新增上用於讀取渠道號的arr包
apply plugin: 'walle'
dependencies {
compile 'com.meituan.android.walle:library:1.1.6'
}
並在當前App的 build.gradle 檔案上配置外掛,預設就行不用改:
walle {
// 指定渠道包的輸出路徑
apkOutputFolder = new File("${project.buildDir}/outputs/channels");
// 定製渠道包的APK的檔名稱
apkFileNameFormat = '${appName}-${packageName}-${channel}-${buildType}-v${versionName}-${versionCode}-${buildTime}.apk';
// 渠道配置檔案
channelFile = new File("${project.getProjectDir()}/channel")
}
建立一個名為channel的txt檔案,配置渠道資訊:
然後就在程式碼中可以通過以下方式獲取渠道資訊並上傳給對應的統計平臺:
String channel = WalleChannelReader.getChannel(this.getApplicationContext());
打渠道包的過程也很簡單:
就是控制檯下的Terminal通過gradle的命令進行打包: gradlew clean assembleReleaseChannels
輸出路徑如下:
然後我們打補丁包的過程仍然是原來的過程進行。
我們來測試一下:
安裝到手機,正常獲取到渠道資訊:
那我們來試試把這些渠道包進行加固,就用360加固重簽名之後再安裝試試:
但是這種方式感覺不夠直接,有沒有更好的方案吶?有的,往下看。
3、360多渠道打包 + 360加固。
首先,我們需要下載最新的360加固助手,登入賬戶。
簽名配置:
多渠道配置:
配置好之後,我們只需在加固應用介面,選擇需要的應用進行加固,它就會自動進行加固,然後打渠道包,再進行重簽名。然後輸出到指定的資料夾,全程自動化操作,而且過程非常快。一個字:爽!
這個過程僅需在tinkerSupport.gradle中修改isProtectedApp = true欄位,然後打好基準包然後用加固助手進行自動化操作即可。
這時我們先來看看專案中的manifest檔案:
再來看看打包之後應用寶渠道的manifest檔案:
可以看到通過360加固助手的多渠道打包,根據對應的統計平臺插入了各個渠道的資訊。
三、總結。
說了那麼多,其實在最終的建議方案就是 先接入Tinker熱更新,然後打基準包,利用.360加固助手進行加固打渠道包在重簽名。上傳其中一個渠道包到bugly平臺上。當有bug時,在基準包基礎上打補丁包,把補丁包上傳到bugly平臺即可。