Tinker的快速集成
不知道从啥时候热修复火了,这也是现在不得不会的技术啊,减少了版本迭代,对于突发情况有很好的应急解决措施,的确是一门很不错的技术。热补丁方案有很多,其中比较出名的有阿里的 AndFix、美团的 Robust 以及 QZone 的补丁,今天聊得是腾讯的Tinker热修复,说实话,其实挺费劲儿的。
Tinker腾讯的解释是:
- 研发实力雄厚;
- 服务全面快速;
- 稳定可靠;
就是很Diao!很NB的意思~~
不扯淡,直接开整:
第一步:添加插件依赖
(切记,这个是Project下的build.gradle)
buildscript {
repositories {
jcenter()
}
dependencies {
// TinkerPatch 插件
classpath "com.tinkerpatch.sdk:tinkerpatch-gradle-plugin:1.1.7"
}
}
第二步:添加库依赖(即app的build.gradle)
dependencies {
// 若使用annotation需要单独引用,对于tinker的其他库都无需再引用
provided("com.tinkerpatch.tinker:tinker-android-anno:1.7.11")
compile("com.tinkerpatch.sdk:tinkerpatch-android-sdk:1.1.7")
}
第三部:配置参数,具体也是在app的build.gradle内配置
def gitSha() {
try {
// String gitRev = 'git rev-parse --short HEAD'.execute(null, project.rootDir).text.trim()
String gitRev = "1008611"
if (gitRev == null) {
throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
return gitRev
} catch (Exception e) {
throw new GradleException("can't get git rev, you should add git to system path or just input test value, such as 'testTinkerId'")
}
}
def bakPath = file("${buildDir}/bakApk/")
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
tinkerEnabled = true
//for normal build
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/app-debug-0518-11-59-06.apk"
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/app-release-0421-12-34-45-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/app-debug-0518-11-59-06-R.txt"
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/app-0421-12-34-45"
}
def getOldApkPath() {
return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}
def getApplyMappingPath() {
return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}
def getApplyResourceMappingPath() {
return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}
def getTinkerIdValue() {
return hasProperty("TINKER_ID") ? TINKER_ID : gitSha()
}
def buildWithTinker() {
return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}
if (buildWithTinker()) {
apply plugin: 'com.tencent.tinker.patch'
tinkerPatch {
/**
* 默认为null
* 将旧的apk和新的apk建立关联
* 从build / bakApk添加apk
*/
oldApk = getOldApkPath()
/**
* 可选,默认'false'
*有些情况下我们可能会收到一些警告
*如果ignoreWarning为true,我们只是断言补丁过程
* case 1:minSdkVersion低于14,但是你使用dexMode与raw。
* case 2:在AndroidManifest.xml中新添加Android组件,
* case 3:装载器类在dex.loader {}不保留在主要的dex,
* 它必须让tinker不工作。
* case 4:在dex.loader {}中的loader类改变,
* 加载器类是加载补丁dex。改变它们是没有用的。
* 它不会崩溃,但这些更改不会影响。你可以忽略它
* case 5:resources.arsc已经改变,但是我们不使用applyResourceMapping来构建
*/
ignoreWarning = true
/**
*可选,默认为“true”
* 是否签名补丁文件
* 如果没有,你必须自己做。否则在补丁加载过程中无法检查成功
* 我们将使用sign配置与您的构建类型
*/
useSign = false
/**
可选,默认为“true”
是否使用tinker构建
*/
tinkerEnable = buildWithTinker()
/**
* 警告,applyMapping会影响正常的android build!
*/
buildConfig {
/**
*可选,默认为'null'
* 如果我们使用tinkerPatch构建补丁apk,你最好应用旧的
* apk映射文件如果minifyEnabled是启用!
* 警告:你必须小心,它会影响正常的组装构建!
*/
applyMapping = getApplyMappingPath()
/**
*可选,默认为'null'
* 很高兴保持资源ID从R.txt文件,以减少java更改
*/
applyResourceMapping = getApplyResourceMappingPath()
/**
*必需,默认'null'
* 因为我们不想检查基地apk与md5在运行时(它是慢)
* tinkerId用于在试图应用补丁时标识唯一的基本apk。
* 我们可以使用git rev,svn rev或者简单的versionCode。
* 我们将在您的清单中自动生成tinkerId
*/
tinkerId = getTinkerIdValue()
/**
*如果keepDexApply为true,则表示dex指向旧apk的类。
* 打开这可以减少dex diff文件大小。
*/
keepDexApply = false
}
dex {
/**
*可选,默认'jar'
* 只能是'raw'或'jar'。对于原始,我们将保持其原始格式
* 对于jar,我们将使用zip格式重新包装dexes。
* 如果你想支持下面14,你必须使用jar
* 或者你想保存rom或检查更快,你也可以使用原始模式
*/
dexMode = "jar"
/**
*必需,默认'[]'
* apk中的dexes应该处理tinkerPatch
* 它支持*或?模式。
*/
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
/**
*必需,默认'[]'
* 警告,这是非常非常重要的,加载类不能随补丁改变。
* 因此,它们将从补丁程序中删除。
* 你必须把下面的类放到主要的dex。
* 简单地说,你应该添加自己的应用程序{@code tinker.sample.android.SampleApplication}
* 自己的tinkerLoader,和你使用的类
*
*/
loader = [
//use sample, let BaseBuildInfo unchangeable with tinker
"tinker.sample.android.app.BaseBuildInfo"
]
}
lib {
/**
可选,默认'[]'
apk中的图书馆应该处理tinkerPatch
它支持*或?模式。
对于资源库,我们只是在补丁目录中恢复它们
你可以得到他们在TinkerLoadResult与Tinker
*/
pattern = ["lib/armeabi/*.so"]
}
res {
/**
*可选,默认'[]'
* apk中的什么资源应该处理tinkerPatch
* 它支持*或?模式。
* 你必须包括你在这里的所有资源,
* 否则,他们不会重新包装在新的apk资源。
*/
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
/**
*可选,默认'[]'
*资源文件排除模式,忽略添加,删除或修改资源更改
* *它支持*或?模式。
* *警告,我们只能使用文件没有relative与resources.arsc
*/
ignoreChange = ["assets/sample_meta.txt"]
/**
*默认100kb
* *对于修改资源,如果它大于'largeModSize'
* *我们想使用bsdiff算法来减少补丁文件的大小
*/
largeModSize = 100
}
packageConfig {
/**
*可选,默认'TINKER_ID,TINKER_ID_VALUE','NEW_TINKER_ID,NEW_TINKER_ID_VALUE'
* 包元文件gen。路径是修补程序文件中的assets / package_meta.txt
* 你可以在您自己的PackageCheck方法中使用securityCheck.getPackageProperties()
* 或TinkerLoadResult.getPackageConfigByName
* 我们将从旧的apk清单为您自动获取TINKER_ID,
* 其他配置文件(如下面的patchMessage)不是必需的
*/
configField("patchMessage", "tinker is sample to use")
/**
*只是一个例子,你可以使用如sdkVersion,品牌,渠道...
* 你可以在SamplePatchListener中解析它。
* 然后你可以使用补丁条件!
*/
configField("platform", "all")
/**
* 补丁版本通过packageConfig
*/
configField("patchVersion", "1.0")
}
//或者您可以添加外部的配置文件,或从旧apk获取元值
//project.tinkerPatch.packageConfig.configField("test1", project.tinkerPatch.packageConfig.getMetaDataFromOldApk("Test"))
//project.tinkerPatch.packageConfig.configField("test2", "sample")
/**
* 如果你不使用zipArtifact或者path,我们只是使用7za来试试
*/
sevenZip {
/**
* 可选,默认'7za'
* 7zip工件路径,它将使用正确的7za与您的平台
*/
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
/**
* 可选,默认'7za'
* 你可以自己指定7za路径,它将覆盖zipArtifact值
*/
// path = "/usr/local/bin/7za"
}
}
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each {flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
/**
* bak apk and mapping
*/
android.applicationVariants.all { variant ->
/**
* task type, you want to bak
*/
def taskName = variant.name
def date = new Date().format("MMdd-HH-mm-ss")
tasks.all {
if ("assemble${taskName.capitalize()}".equalsIgnoreCase(it.name)) {
it.doLast {
copy {
def fileNamePrefix = "${project.name}-${variant.baseName}"
def newFileNamePrefix = hasFlavors ? "${fileNamePrefix}" : "${fileNamePrefix}-${date}"
def destPath = hasFlavors ? file("${bakPath}/${project.name}-${date}/${variant.flavorName}") : bakPath
from variant.outputs.outputFile
into destPath
rename { String fileName ->
fileName.replace("${fileNamePrefix}.apk", "${newFileNamePrefix}.apk")
}
from "${buildDir}/outputs/mapping/${variant.dirName}/mapping.txt"
into destPath
rename { String fileName ->
fileName.replace("mapping.txt", "${newFileNamePrefix}-mapping.txt")
}
from "${buildDir}/intermediates/symbols/${variant.dirName}/R.txt"
into destPath
rename { String fileName ->
fileName.replace("R.txt", "${newFileNamePrefix}-R.txt")
}
}
}
}
}
}
project.afterEvaluate {
//sample use for build all flavor for one time
if (hasFlavors) {
task(tinkerPatchAllFlavorRelease) {
group = 'tinker'
def originOldPath = getTinkerBuildFlavorDirectory()
for (String flavor : flavors) {
def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Release")
dependsOn tinkerTask
def preAssembleTask = tasks.getByName("process${flavor.capitalize()}ReleaseManifest")
preAssembleTask.doFirst {
String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 15)
project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release.apk"
project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-mapping.txt"
project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-release-R.txt"
}
}
}
task(tinkerPatchAllFlavorDebug) {
group = 'tinker'
def originOldPath = getTinkerBuildFlavorDirectory()
for (String flavor : flavors) {
def tinkerTask = tasks.getByName("tinkerPatch${flavor.capitalize()}Debug")
dependsOn tinkerTask
def preAssembleTask = tasks.getByName("process${flavor.capitalize()}DebugManifest")
preAssembleTask.doFirst {
String flavorName = preAssembleTask.name.substring(7, 8).toLowerCase() + preAssembleTask.name.substring(8, preAssembleTask.name.length() - 13)
project.tinkerPatch.oldApk = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug.apk"
project.tinkerPatch.buildConfig.applyMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-mapping.txt"
project.tinkerPatch.buildConfig.applyResourceMapping = "${originOldPath}/${flavorName}/${project.name}-${flavorName}-debug-R.txt"
}
}
}
}
}
上面已有对应的注释与解释,其中bakPath的意思就是生成Apk的位置,这里我们设置的位置是app的build文件夹下的bakApk,如下图
如图apk连带名称带路径需匹配在tinkerOldApkPath, R.txt则需要匹配tinkerApplyResourcePath;
我们项目中需要写的Application也发生了变化,引入Tinker后需要将我们的Application继承DefaultApplicationLike,出乎意料的是这个类并没有继承Application,这里运动了注解反射,大家可以参照以下写法:
@SuppressWarnings("unused")
@DefaultLifeCycle(application = "demo.com.tinkerdemo.Application",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class MyApp extends DefaultApplicationLike {
public MyApp(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent, Resources[] resources, ClassLoader[] classLoader, AssetManager[] assetManager) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent, resources, classLoader, assetManager);
}
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
MultiDex.install(base);
TinkerInstaller.install(this);
}
}
其中我的注解指向的是我的包名+Application,当然大家不要忽略AndroidManifest.xml清单文件中注册,这里没法直接生成了,也需要加包名,如下图:
可能仍会爆红,重构一下项目或者clean一下就好了;
当你解决完你的Bug或更改后,你需将两个包进行比较,生成我们的补丁差异文件:pacth_signed_7zip.apk,这就是我们需要推送的差异补丁包了。
具体生成方法有两种,一种是Terminal命令行,还有一种方法是如下图,easy~
生存的补丁文件下outputs目录下:
对了,忘记说了,在你的主Activity中记得指明你要推送的补丁包路径,这样才能将你之前的Apk和补丁包融合一起啊~
讲解的不精,大家有不懂还是需要去看API,放个地址吧:http://www.tinkerpatch.com/Docs/SDK