微信Tinker_從1.7.0開源開始,本人就已經(jīng)密切關(guān)注它的發(fā)展棋电,并在1.7.0就已經(jīng)成功接入并運(yùn)行(未在生產(chǎn)環(huán)境使用)懊亡,現(xiàn)在Tinker已經(jīng)更新到1.7.7了。在微信Tinker之前奋献,筆者曾經(jīng)在阿里andfix的泥潭掙扎許久,現(xiàn)在已放棄旺上。
注: 本文需要讀者需要擁有Tinker接入經(jīng)驗(yàn)
Bugly
Tinker
multidex
packer
自定義task
關(guān)于Bugly的熱修復(fù)
騰訊Bugly的使用初衷是在線關(guān)注APP的錯誤日志瓶蚂,在Tinker開源并基于穩(wěn)定后,Bugly就搭上了Tinker的順風(fēng)車(雖然它們都是騰訊出品)宣吱。
單獨(dú)接入過Tinker的猿應(yīng)該都知道窃这,Tinker僅提供補(bǔ)丁功能,并不支持管理功能征候,如果決定使用Tinker補(bǔ)丁功能杭攻,就必須自己實(shí)現(xiàn)后臺補(bǔ)丁管理祟敛。一般公司,時間就是生命的環(huán)境下是不會有這個考慮的兆解。然后就是后來馆铁,TinkerPatch平臺應(yīng)運(yùn)而生,但是并沒有解決上述問題锅睛,據(jù)我了解埠巨,TinkerPatch是一個個人項(xiàng)目(雖然是微信的人在維護(hù)),而且計(jì)劃收費(fèi)现拒,這又是一般公司不能接受的辣垒。
在目前沒有項(xiàng)目計(jì)劃,又需要跟上時代跟上技術(shù)的的尷尬大前提下印蔬,我選擇了免費(fèi)勋桶、不需要自己搭建后臺的Bugly熱修復(fù),并決定應(yīng)用到生產(chǎn)環(huán)境中扛点。
開始接入
使用過Bugly的猿都知道哥遮,com.tencent.bugly:crashreport_upgrade:x.y.z
這個庫提供了異常上傳,版本更新及熱修復(fù)功能(如果你不知道或沒有接入陵究,請到Bugly SDK【升級SDK包】查看)的功能。筆者就是從這個庫接入的奥帘。
前提
<font color='red'>筆者項(xiàng)目通過Zip comment方式(Tinker力薦)打多渠道包铜邮,故接入細(xì)節(jié)涉及到packer的使用,接入及使用都按照筆者工程的需求來做的寨蹋,這里僅提供一點(diǎn)思路松蒜。</font>
工程配置
1. packer配置
工程下的build.gradle
配置依賴 -> 引入packer插件
classpath 'com.mcxiaoke.gradle:packer-ng:1.0.8'
項(xiàng)目下的build.gradle
配置依賴 -> 引入packer操作渠道的工具類
compile 'com.mcxiaoke.gradle:packer-helper:1.0.8'
項(xiàng)目下的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
}
在項(xiàng)目目錄下新建/復(fù)制markets.txt文件已旧,文件中保存了需要打包的渠道信息秸苗。如圖:
上述內(nèi)容配置完成并完成gradle同步后,出現(xiàn)如下圖的任務(wù)运褪,則說明配置成功惊楼,執(zhí)行任務(wù),就可成功打出需要渠道的apk包秸讹√戳可通過PackerNg.getMarket(context)
方式讀取渠道類型。
2. Bugly配置
這一節(jié)基本看著Bugly的接入文檔就可以完成璃诀,這里只做簡單概述弧可。
項(xiàng)目中的build.gradle
中引入依賴 -> 異常上報(bào)、升級劣欢、熱修復(fù)
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
}
}
上述內(nèi)容配置完成并完成gradle同步后,出現(xiàn)如下圖的任務(wù)校套,則說明配置成功价脾,如果項(xiàng)目進(jìn)行混淆、bugly
的upload=true
,運(yùn)行assembleRelease
<font color='red'>及相關(guān)</font>任務(wù)搔确,則生成的mapping.txt文件將自動上傳至bugly后臺彼棍。
tinker支持配置
在配置完成Bugly相關(guān)內(nèi)容后,目前生效的功能僅為異常上報(bào)及升級膳算,仍然不能支持熱修復(fù)座硕。下面將簡述(Bugly熱修復(fù)文檔詳情)Bugly熱修復(fù)的配置.
工程的build.gradle
文件中引入依賴 -> tinker支持插件
classpath "com.tencent.bugly:tinker-support:1.0.3"
項(xiàng)目的build.gradle
文件引入依賴 -> 支持dex分包
compile "com.android.support:multidex:1.0.1"
項(xiàng)目目錄下,新建tinker-support.gradle腳本文件
腳本內(nèi)容介紹
- 本地插件涕蜂,定義一些基本數(shù)據(jù)及工具函數(shù)
apply from: 'config.gradle'
apply from: 'utils.gradle'
- 目標(biāo)文件存儲目錄
def bakPath = file("archives")
def appName = 'base'
- tinker-patch插件配置
apply plugin: 'com.tencent.bugly.tinker-support'
tinkerSupport {
// 開啟tinker-support插件华匾,默認(rèn)值true
enable = true
// 指定歸檔目錄,默認(rèn)值當(dāng)前module的子目錄tinker
autoBackupApkDir = "${bakPath}"
// 是否啟用覆蓋tinkerPatch配置功能机隙,默認(rèn)值false
// 開啟后tinkerPatch配置不生效蜘拉,即無需添加tinkerPatch
overrideTinkerPatchConfiguration = true
// 編譯補(bǔ)丁包時,必需指定基線版本的apk有鹿,默認(rèn)值為空
// 如果為空旭旭,則表示不是進(jìn)行補(bǔ)丁包的編譯
// @{link tinkerPatch.oldApk }
baseApk = "${bakPath}/${appName}/app-release.apk"
// 對應(yīng)tinker插件applyMapping
baseApkProguardMapping = "${bakPath}/${appName}/app-release-mapping.txt"
// 對應(yīng)tinker插件applyResourceMapping
baseApkResourceMapping = "${bakPath}/${appName}/app-release-R.txt"
// 唯一標(biāo)識當(dāng)前版本
tinkerId = "${VERSION_NAME}"
// 是否開啟代理Application,設(shè)置之后無須改造Application葱跋,默認(rèn)為false
enableProxyApplication = true
}
- 自定義清理任務(wù)【packer持寄、tinker生產(chǎn)文件】
task tinkerClean() {
def destArchives = file("${bakPath}")
group 'tinker-ext'
description "Delete files in ${destArchives} include ${destArchives} self."
doFirst {
delete(destArchives)
}
}
- 自定義產(chǎn)出任務(wù)【packer渠道包及tinker基礎(chǔ)包】
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任務(wù)【產(chǎn)出tinker補(bǔ)丁】
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插件,默認(rèn)值true
enable = true
// 指定歸檔目錄娱俺,默認(rèn)值當(dāng)前module的子目錄tinker
autoBackupApkDir = "${bakPath}"
// 是否啟用覆蓋tinkerPatch配置功能稍味,默認(rèn)值false
// 開啟后tinkerPatch配置不生效,即無需添加tinkerPatch
overrideTinkerPatchConfiguration = true
// 編譯補(bǔ)丁包時荠卷,必需指定基線版本的apk模庐,默認(rèn)值為空
// 如果為空,則表示不是進(jìn)行補(bǔ)丁包的編譯
// @{link tinkerPatch.oldApk }
baseApk = "${bakPath}/${appName}/app-release.apk"
// 對應(yīng)tinker插件applyMapping
baseApkProguardMapping = "${bakPath}/${appName}/app-release-mapping.txt"
// 對應(yīng)tinker插件applyResourceMapping
baseApkResourceMapping = "${bakPath}/${appName}/app-release-R.txt"
// 唯一標(biāo)識當(dāng)前版本
tinkerId = "${VERSION_NAME}"
// 是否開啟代理Application油宜,設(shè)置之后無須改造Application掂碱,默認(rèn)為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最終提供如下圖的任務(wù)
最終筆者通過自定義的3個任務(wù)tinkerClean
tinkerGo
tinkerPrepare
來進(jìn)行項(xiàng)目的打包(多渠道)發(fā)布,及補(bǔ)丁的生成
- tinkerPrepare效果
- tinkerGo效果
上述過程已經(jīng)完成了Bugly熱修復(fù)接入的全部工作(至少可以以正常流程來打補(bǔ)丁了),下面將再次簡述工程中的代碼接入流程验庙。
代碼接入
Bugly提供了tinker原生的接入方式(TinkerApplication或者ApplicationLike方式)及一鍵接入顶吮,這里介紹下一鍵接入方式
設(shè)置tinker-support支持使用代理Application
// 是否開啟代理Application,設(shè)置之后無須改造Application粪薛,默認(rèn)為false
enableProxyApplication = true
加載tinker組件
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
Beta.installTinker();
//設(shè)置自定義report監(jiān)聽
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);
}
最后
文章寫到這里悴了,或許已經(jīng)偏離的我下筆時的初衷,這一方面是我自己水平不足,另一方面也是工程+熱修復(fù)這一個過程融合的難度所致湃交。
我希望:
- 文章對您有所幫助
- 請您不吝賜教
- 接下來有提高篇和深入篇
CatPaw 2017-01-20 于京昆高鐵