微信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 于京昆高铁
网友评论
utils.gradle 里有些工具函数,主要用到了 delete 方法
def delete(path) {
final String METHOD_NAME = "utils.gradle#delete";
if (path == null || "".equalsIgnoreCase(path)) {
return
}
def dest = null
if (path instanceof String) {
dest = file(path)
} else if (path instanceof File) {
dest = path
}
if (dest == null) {
throw new NullPointerException("${METHOD_NAME} process null object!!!")
}
if (dest.isFile()) {
dest.delete()
} else {
dest.listFiles().each {
delete(it)
}
dest.delete()
}
}
ext {
delete = this.&delete
}