微信Tinker_從1.7.0開源開始,本人就已經密切關注它的發展,并在1.7.0就已經成功接入并運行(未在生產環境使用),現在Tinker已經更新到1.7.7了。在微信Tinker之前,筆者曾經在阿里andfix的泥潭掙扎許久,現在已放棄。
注: 本文需要讀者需要擁有Tinker接入經驗
Bugly
Tinker
multidex
packer
自定義task
關于Bugly的熱修復
騰訊Bugly的使用初衷是在線關注APP的錯誤日志,在Tinker開源并基于穩定后,Bugly就搭上了Tinker的順風車(雖然它們都是騰訊出品)。
單獨接入過Tinker的猿應該都知道,Tinker僅提供補丁功能,并不支持管理功能,如果決定使用Tinker補丁功能,就必須自己實現后臺補丁管理。一般公司,時間就是生命的環境下是不會有這個考慮的。然后就是后來,TinkerPatch平臺應運而生,但是并沒有解決上述問題,據我了解,TinkerPatch是一個個人項目(雖然是微信的人在維護),而且計劃收費,這又是一般公司不能接受的。
在目前沒有項目計劃,又需要跟上時代跟上技術的的尷尬大前提下,我選擇了免費、不需要自己搭建后臺的Bugly熱修復,并決定應用到生產環境中。
開始接入
使用過Bugly的猿都知道,com.tencent.bugly:crashreport_upgrade:x.y.z
這個庫提供了異常上傳,版本更新及熱修復功能(如果你不知道或沒有接入,請到Bugly SDK【升級SDK包】查看)的功能。筆者就是從這個庫接入的。
前提
<font color='red'>筆者項目通過Zip comment方式(Tinker力薦)打多渠道包,故接入細節涉及到packer的使用,接入及使用都按照筆者工程的需求來做的,這里僅提供一點思路。</font>
工程配置
1. packer配置
工程下的build.gradle
配置依賴 -> 引入packer插件
classpath 'com.mcxiaoke.gradle:packer-ng:1.0.8'
項目下的build.gradle
配置依賴 -> 引入packer操作渠道的工具類
compile 'com.mcxiaoke.gradle:packer-helper:1.0.8'
項目下的build.gradle
配置packer插件
//混淆配置
signingConfigs {
release {
keyAlias KEY_ALISA
keyPassword KEY_PASSWORD
storeFile KEY_STORE_FILE
storePassword STORE_PASSWORD
// 1. Gradle版本 >= 2.14.1
// 2. Android Gradle Plugin 版本 >= 2.2.0
// 作用是只使用舊版簽名,禁用V2版簽名模式
v2SigningEnabled false
}
}
//插件配置
apply plugin: 'packer'
packer {
def date = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss").format(new Date())
checkSigningConfig = true
checkZipAlign = true
archiveOutput = file("archives")
archiveNameFormat = '${appPkg}-${flavorName}-${buildType}-v${versionName}-${fileMD5}-' + "${PRODUCT_TYPE}-" + date
}
在項目目錄下新建/復制markets.txt文件,文件中保存了需要打包的渠道信息。如圖:
上述內容配置完成并完成gradle同步后,出現如下圖的任務,則說明配置成功,執行任務,就可成功打出需要渠道的apk包。可通過PackerNg.getMarket(context)
方式讀取渠道類型。
2. Bugly配置
這一節基本看著Bugly的接入文檔就可以完成,這里只做簡單概述。
項目中的build.gradle
中引入依賴 -> 異常上報、升級、熱修復
compile "com.tencent.bugly:crashreport_upgrade:1.2.2"
為了方便Bugly的管理,筆者將Bugly的配置放置到了bugly-support.grale文件中(如果你不懂gradle,請看這里)
apply from: 'config.gradle'
if (BUGLY_ENABLE) {
apply plugin: 'bugly'
bugly {
appId = BUGLY_APPID
appKey = BUGLY_APPKEY
execute = BUGLY_EXECUTE
upload = BUGLY_UPLOAD
}
}
上述內容配置完成并完成gradle同步后,出現如下圖的任務,則說明配置成功,如果項目進行混淆、bugly
的upload=true
,運行assembleRelease
<font color='red'>及相關</font>任務,則生成的mapping.txt文件將自動上傳至bugly后臺。
tinker支持配置
在配置完成Bugly相關內容后,目前生效的功能僅為異常上報及升級,仍然不能支持熱修復。下面將簡述(Bugly熱修復文檔詳情)Bugly熱修復的配置.
工程的build.gradle
文件中引入依賴 -> tinker支持插件
classpath "com.tencent.bugly:tinker-support:1.0.3"
項目的build.gradle
文件引入依賴 -> 支持dex分包
compile "com.android.support:multidex:1.0.1"
項目目錄下,新建tinker-support.gradle腳本文件
腳本內容介紹
- 本地插件,定義一些基本數據及工具函數
apply from: 'config.gradle'
apply from: 'utils.gradle'
- 目標文件存儲目錄
def bakPath = file("archives")
def appName = 'base'
- tinker-patch插件配置
apply plugin: 'com.tencent.bugly.tinker-support'
tinkerSupport {
// 開啟tinker-support插件,默認值true
enable = true
// 指定歸檔目錄,默認值當前module的子目錄tinker
autoBackupApkDir = "${bakPath}"
// 是否啟用覆蓋tinkerPatch配置功能,默認值false
// 開啟后tinkerPatch配置不生效,即無需添加tinkerPatch
overrideTinkerPatchConfiguration = true
// 編譯補丁包時,必需指定基線版本的apk,默認值為空
// 如果為空,則表示不是進行補丁包的編譯
// @{link tinkerPatch.oldApk }
baseApk = "${bakPath}/${appName}/app-release.apk"
// 對應tinker插件applyMapping
baseApkProguardMapping = "${bakPath}/${appName}/app-release-mapping.txt"
// 對應tinker插件applyResourceMapping
baseApkResourceMapping = "${bakPath}/${appName}/app-release-R.txt"
// 唯一標識當前版本
tinkerId = "${VERSION_NAME}"
// 是否開啟代理Application,設置之后無須改造Application,默認為false
enableProxyApplication = true
}
- 自定義清理任務【packer、tinker生產文件】
task tinkerClean() {
def destArchives = file("${bakPath}")
group 'tinker-ext'
description "Delete files in ${destArchives} include ${destArchives} self."
doFirst {
delete(destArchives)
}
}
- 自定義產出任務【packer渠道包及tinker基礎包】
def dest = file("${bakPath}/${appName}/")
task tinkerPrepare() {
dependsOn 'apkRelease'
group 'tinker-ext'
description 'Release only - Generate apk file include: packer,generate tinker bak reouserce fiels.'
doLast {
copy {
if (!dest.exists()) {
dest.mkdirs()
}
from "${buildDir}/outputs/apk/app-release.apk"
into dest
def destApk = file("${dest}app-release.apk")
if (destApk.exists()) {
println "Tinker bakApk file ${destApk.absolutePath} done."
}
from "${buildDir}/outputs/mapping/release/app-mapping.txt"
into dest
rename { String fileName ->
fileName.replace("app-mapping.txt", "app-release-mapping.txt")
}
def destMappingFile = file("${dest}app-release-mapping.txt")
if (destMappingFile.exists()) {
println "Tinker mapping file ${destMappingFile.absolutePath} done."
}
from "${buildDir}/intermediates/symbols/release/R.txt"
into "${bakPath}/${appName}/"
rename { String fileName ->
fileName.replace("R.txt", "app-release-R.txt")
}
def destResourceFile = file("${dest}app-release-R.txt")
if (destResourceFile.exists()) {
println "Tinker mapping file ${destResourceFile.absolutePath} done."
}
bakPath.listFiles(new FilenameFilter() {
@Override
boolean accept(File dir, String filename) {
return filename.startsWith("app-")
}
}).each {
if (it.isDirectory()) {
it.listFiles().each {
it.delete()
}
it.delete()
}
}
println 'Tinker prepare done.'
}
}
}
- 自定義patch任務【產出tinker補丁】
task tinkerGo {
dependsOn 'tinkerPatchRelease'
description "Release only - Generate tinker patch and copy to ${dest} patch."
group 'tinker-ext'
doLast {
copy {
def patch = file("${dest}/patch/")
if (!patch.exists()) {
patch.mkdirs()
}
file("${buildDir}/outputs/patch/release/").listFiles().each {
from it.absolutePath
into file("${patch}")
}
bakPath.listFiles(new FilenameFilter() {
@Override
boolean accept(File dir, String filename) {
return filename.startsWith("app-")
}
}).each {
if (it.isDirectory()) {
it.listFiles().each {
it.delete()
}
it.delete()
}
}
}
}
}
- 完整文件代碼
apply from: 'config.gradle'
apply from: 'utils.gradle'
def bakPath = file("archives")
def appName = 'base'
if (TINKER_ENABLE) {
apply plugin: 'com.tencent.bugly.tinker-support'
tinkerSupport {
// 開啟tinker-support插件,默認值true
enable = true
// 指定歸檔目錄,默認值當前module的子目錄tinker
autoBackupApkDir = "${bakPath}"
// 是否啟用覆蓋tinkerPatch配置功能,默認值false
// 開啟后tinkerPatch配置不生效,即無需添加tinkerPatch
overrideTinkerPatchConfiguration = true
// 編譯補丁包時,必需指定基線版本的apk,默認值為空
// 如果為空,則表示不是進行補丁包的編譯
// @{link tinkerPatch.oldApk }
baseApk = "${bakPath}/${appName}/app-release.apk"
// 對應tinker插件applyMapping
baseApkProguardMapping = "${bakPath}/${appName}/app-release-mapping.txt"
// 對應tinker插件applyResourceMapping
baseApkResourceMapping = "${bakPath}/${appName}/app-release-R.txt"
// 唯一標識當前版本
tinkerId = "${VERSION_NAME}"
// 是否開啟代理Application,設置之后無須改造Application,默認為false
enableProxyApplication = true
}
def dest = file("${bakPath}/${appName}/")
task tinkerClean() {
def destArchives = file("${bakPath}")
group 'tinker-ext'
description "Delete files in ${destArchives} include ${destArchives} self."
doFirst {
delete(destArchives)
}
}
task tinkerPrepare() {
dependsOn 'apkRelease'
group 'tinker-ext'
description 'Release only - Generate apk file include: packer,generate tinker bak reouserce fiels.'
doLast {
copy {
if (!dest.exists()) {
dest.mkdirs()
}
from "${buildDir}/outputs/apk/app-release.apk"
into dest
def destApk = file("${dest}app-release.apk")
if (destApk.exists()) {
println "Tinker bakApk file ${destApk.absolutePath} done."
}
from "${buildDir}/outputs/mapping/release/app-mapping.txt"
into dest
rename { String fileName ->
fileName.replace("app-mapping.txt", "app-release-mapping.txt")
}
def destMappingFile = file("${dest}app-release-mapping.txt")
if (destMappingFile.exists()) {
println "Tinker mapping file ${destMappingFile.absolutePath} done."
}
from "${buildDir}/intermediates/symbols/release/R.txt"
into "${bakPath}/${appName}/"
rename { String fileName ->
fileName.replace("R.txt", "app-release-R.txt")
}
def destResourceFile = file("${dest}app-release-R.txt")
if (destResourceFile.exists()) {
println "Tinker mapping file ${destResourceFile.absolutePath} done."
}
bakPath.listFiles(new FilenameFilter() {
@Override
boolean accept(File dir, String filename) {
return filename.startsWith("app-")
}
}).each {
if (it.isDirectory()) {
it.listFiles().each {
it.delete()
}
it.delete()
}
}
println 'Tinker prepare done.'
}
}
}
task tinkerGo {
dependsOn 'tinkerPatchRelease'
description "Release only - Generate tinker patch and copy to ${dest} patch."
group 'tinker-ext'
doLast {
copy {
def patch = file("${dest}/patch/")
if (!patch.exists()) {
patch.mkdirs()
}
file("${buildDir}/outputs/patch/release/").listFiles().each {
from it.absolutePath
into file("${patch}")
}
bakPath.listFiles(new FilenameFilter() {
@Override
boolean accept(File dir, String filename) {
return filename.startsWith("app-")
}
}).each {
if (it.isDirectory()) {
it.listFiles().each {
it.delete()
}
it.delete()
}
}
}
}
}
}
上述的fucking code最終提供如下圖的任務
最終筆者通過自定義的3個任務tinkerClean
tinkerGo
tinkerPrepare
來進行項目的打包(多渠道)發布,及補丁的生成
- tinkerPrepare效果
- tinkerGo效果
上述過程已經完成了Bugly熱修復接入的全部工作(至少可以以正常流程來打補丁了),下面將再次簡述工程中的代碼接入流程。
代碼接入
Bugly提供了tinker原生的接入方式(TinkerApplication或者ApplicationLike方式)及一鍵接入,這里介紹下一鍵接入方式
設置tinker-support支持使用代理Application
// 是否開啟代理Application,設置之后無須改造Application,默認為false
enableProxyApplication = true
加載tinker組件
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
Beta.installTinker();
//設置自定義report監聽
TinkerManager.getInstance().setTinkerReport(new TinkerReporterImpl());
}
配置bugly
@Override
public void onCreate(){
BuglyStrategy strategy = new BuglyStrategy();
strategy.setAppChannel(PackerNg.getMarket(this));
logger.i(TAG, "Channel: %s", strategy.getAppChannel());
Bugly.init(this, "xxx", Config.DEBUG_MODE, strategy);
Bugly.setIsDevelopmentDevice(this, !Config.DEBUG_MODE);
}
最后
文章寫到這里,或許已經偏離的我下筆時的初衷,這一方面是我自己水平不足,另一方面也是工程+熱修復這一個過程融合的難度所致。
我希望:
- 文章對您有所幫助
- 請您不吝賜教
- 接下來有提高篇和深入篇
CatPaw 2017-01-20 于京昆高鐵