背景
截止17年3月11日,百川Hotfix1.x版本不支持安卓7.0系统(2.0版本跳票了2个月还没音讯),导致很多华为用户无法享受到热更新,因此放弃使用。
腾讯的热修复框架tinker涵盖系统广、可修复资源类型多等优势,我们选择其一站式的热修复平台tinker patch进行接入。
本次集成步骤,基于tinker Patch 1.1.4版本,使用android studio开发工具。官方的集成实践
准备工作
gradle文件
在工程根目录的build.gradle里添加tinkerPatch引用
classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:1.1.4"
在项目的app工程目录下
- 添加
gradle.properties
文件,添加版本号作为全局变量
version=4.0.4
- 添加
tinkerpatch.gradle
文件,封装了热修复所需要的函数,因为集成了andresguard资源混淆,因此文件略长
apply plugin: 'tinkerpatch-support'
//每次发热修复修改原包位置
def baseInfo = "app-4.0.6-0310-18-00-37"
def bakPath = file("${buildDir}/bakApk/")
def variantName = "release"
/**
* 对于插件各参数的详细解析请参考
* http://tinkerpatch.com/Docs/SDK
*/
tinkerpatchSupport {
appKey = "你的tinkerPatch key"
/** 可以在debug的时候关闭 tinkerPatch, isRelease() 可以判断BuildType是否为Release **/
tinkerEnable = isRelease()
reflectApplication = true
autoBackupApkPath = "${bakPath}"
/** 注意: 若发布新的全量包, appVersion一定要更新 **/
appVersion = version
def pathPrefix = "${bakPath}/${baseInfo}/${variantName}/"
def name = "${project.name}-${variantName}"
baseApkFile = "${pathPrefix}/${name}.apk"
baseProguardMappingFile = "${pathPrefix}/${name}-mapping.txt"
baseResourceRFile = "${pathPrefix}/${name}-R.txt"
/**
* 若有编译多flavors需求, 可以参照: https://github.com/TinkerPatch/tinkerpatch-flavors-sample
* 注意: 除非你不同的flavor代码是不一样的,不然建议采用zip comment或者文件方式生成渠道信息(相关工具:walle 或者 packer-ng)
**/
}
/**
* 用于用户在代码中判断tinkerPatch是否被使能
*/
android {
defaultConfig {
buildConfigField "boolean", "TINKER_ENABLE", "${tinkerpatchSupport.tinkerEnable}"
}
}
/**
* 一般来说,我们无需对下面的参数做任何的修改
* 对于各参数的详细介绍请参考:
* https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97
*/
tinkerPatch {
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
}
}
import java.util.regex.Matcher
import java.util.regex.Pattern
/**
* 如果只想在Release中打开tinker,可以把tinkerEnable赋值为这个函数的return
* @return 是否为release
*/
def isRelease() {
Gradle gradle = getGradle()
String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
Pattern pattern;
if (tskReqStr.contains("assemble")) {
println tskReqStr
pattern = Pattern.compile("assemble(\\w*)(Release|Debug)")
} else {
pattern = Pattern.compile("generate(\\w*)(Release|Debug)")
}
Matcher matcher = pattern.matcher(tskReqStr)
if (matcher.find()) {
String task = matcher.group(0).toLowerCase()
println("[BuildType] Current task: " + task)
return task.contains("release")
} else {
println "[BuildType] NO MATCH FOUND"
return true;
}
}
apply plugin: 'AndResGuard'
andResGuard {
mappingFile = null
use7zip = true
useSign = true
keepRoot = false
// add <yourpackagename>.R.drawable.icon into whitelist.
// because the launcher will get the icon with his name
whiteList = [
// your icon
"R.drawable.icon",
// for fabric
"R.string.com.crashlytics.*",
// for umeng update
"R.string.umeng*",
"R.string.UM*",
"R.string.tb_*",
"R.string.rc_*",
"R.layout.umeng*",
"R.layout.tb_*",
"R.layout.rc_*",
"R.drawable.umeng*",
"R.drawable.tb_*",
"R.drawable.rc_*",
"R.drawable.u1*",
"R.drawable.u2*",
"R.anim.umeng*",
"R.color.umeng*",
"R.color.tb_*",
"R.color.rc_*",
"R.style.*UM*",
"R.style.umeng*",
"R.style.rc_*",
"R.id.umeng*",
"R.id.rc_*",
// umeng share for sina
"R.drawable.sina*",
// for google-services.json
"R.string.google_app_id",
"R.string.gcm_defaultSenderId",
"R.string.default_web_client_id",
"R.string.ga_trackingId",
"R.string.firebase_database_url",
"R.string.google_api_key",
"R.string.google_crash_reporting_api_key",
"R.dimen.rc_*"
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
"resources.arsc"
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.1.16'
//path = "/usr/local/bin/7za"
}
}
project.afterEvaluate {
def date = new Date().format("MMdd-HH-mm-ss")
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
String name = variant.name.toLowerCase()
String destFilePrefix = "${project.name}-${name}"
// find resguard task first
def resguardTask = project.tasks.findByName("resguard${taskName.capitalize()}")
if (resguardTask == null) {
println("resguardTask not found, just return")
return
}
def tinkerPatchTask = project.tasks.findByName("tinkerPatch${taskName.capitalize()}")
if (tinkerPatchTask == null) {
println("resguardTask not found, just return")
return
}
resguardTask.doFirst {
def resMapping = "${bakPath}/${baseInfo}/${taskName}/${project.name}-${taskName}-resource_mapping.txt"
File mapping = new File(resMapping)
if (mapping.exists()) {
println("change resguardTask mapping file to ${resMapping}")
project.extensions.andResGuard.mappingFile = file(resMapping)
}
}
tinkerPatchTask.doFirst {
def buildApkPath = "${buildDir}/outputs/apk/AndResGuard_${project.getName()}-${taskName}/${project.getName()}-${taskName}_signed_7zip_aligned.apk"
println("change tinkerPatchTask buildApkPath to resugurad output ${buildApkPath}")
tinkerPatchTask.buildApkPath = buildApkPath
println("change tinkerPatchTask baseApk to ${destFilePrefix}-resuguard.apk")
project.extensions.tinkerPatch.oldApk = "${bakPath}/${baseInfo}/${variantName}/${destFilePrefix}-resuguard.apk"
}
tinkerPatchTask.dependsOn resguardTask
resguardTask.doLast {
String buildType = variant.buildType.name.toLowerCase()
if (!name.equalsIgnoreCase(buildType) && name.endsWith(buildType)) {
name = name - buildType + "-${buildType}"
}
String mAppVersion = project.extensions.tinkerpatchSupport.appVersion
String destPath = "${bakPath}/${project.name}-${mAppVersion}-${date}/${name}/"
copy {
from "${buildDir}/outputs/apk/AndResGuard_${project.getName()}-${taskName}/${project.getName()}-${taskName}_signed_7zip_aligned.apk"
into file("${destPath}/")
rename { String fileName ->
fileName.replace("${project.getName()}-${taskName}_signed_7zip_aligned.apk", "${destFilePrefix}-resuguard.apk")
}
from "${buildDir}/outputs/apk/AndResGuard_${project.getName()}-${taskName}/resource_mapping_${project.getName()}-${taskName}.txt"
into file("${destPath}/")
rename { String fileName ->
fileName.replace("resource_mapping_${project.getName()}-${taskName}.txt", "${destFilePrefix}-resource_mapping.txt")
}
}
}
}
}
- 打开app的
build.gradle
文件,添加第二步gradle文件引用
apply from: 'tinkerpatch.gradle'
依赖关系添加tinker
provided("com.tencent.tinker:tinker-android-anno:1.7.7")
compile("com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.1.4")
代码集成
在项目的application
文件,添加下述代码,然后在oncreate()
方法,调用initTinker()
即可
private ApplicationLike tinkerApplicationLike;
private void initTinker() {
// 我们可以从这里获得Tinker加载过程的信息
if (BuildConfig.TINKER_ENABLE) {
tinkerApplicationLike = TinkerPatchApplicationLike.getTinkerPatchApplicationLike();
// 初始化TinkerPatch SDK
TinkerPatch.init(tinkerApplicationLike)
.reflectPatchLibrary()
.setPatchRollbackOnScreenOff(true)
.setPatchRestartOnSrceenOff(true);
TinkerPatch.with().fetchPatchUpdate(true);
}
}
上述步骤可以在官方集成demo中查看,本文多了andresguard步骤,因此会略有出入。
至此tinker已经集成完毕了,接下来是打热修复patch步骤
热修复
- 在tinkerPatch.gradle文件里,修改
baseInfo
路径,其指定了老apk包的路径,可以在apk输出目录的bakApk
目录下复制文件夹名即可 - 所有
build varianties
切换成release
模式,找到gradle任务的tinker patch release
任务双击运行
- 在apk输出目录下,打开
tinkerPatch
目录,找到名叫patch_signed_7zip.apk的补丁包,上传到tinkerPatch(选择开发预览模式)。 - 本地验证,下载tinkerPatch的debug tools并开启,重启app开始检查热更新,查看log输出,如果提示等待重启,则锁屏,解锁,即可看到热修复生效。
- 本地验证通过之后,切换补丁发布模式为全量发布
后记
- 每次发版,修改版本号均要在我们添加的
gradle.properties
里修改 - 使用极光推送的自定义消息体,通知客户端调用
TinkerPatch.with().fetchPatchUpdate(true);
访问热修复接口,可以大大提高热修复的及时性,内容里面使用hotfix_1.0.0
,客户端通过版本号匹配与否来决定是否需要访问热修复。因为tinker patch的费用与更新请求访问量挂钩。 - 遇到线上包需要发1个以上补丁时,每次补丁需要涵盖之前所有修复的内容,也就是说,每次补丁的基准包都是线上包的代码。
tinkerPatch自动会请求最新的补丁。