前言:
当下市面上比较流行的热修复技术有很多,其中比较出名的有阿里的AndFix、美团的Robust以及腾讯的Tinker。在这里我选取Tinker作为学习对象,除了它最为强大的功能外,尤其喜欢Tinker官方文档中的一句话“Tinker已运行在微信的数亿Android设备上,那么为什么你不使用Tinker呢?”。哈哈下面就开始我们的Tinker学习之旅。
Tinker是什么?
Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码、So库以及资源,让应用能够在不需要重新安装的情况下实现更新。当然,你也可以使用Tinker来更新你的插件。
为什么使用Tinker?
当前市面的热补丁方案有很多,其中比较出名的有阿里的AndFix、美团的Robust以及QZone的超级补丁方案。但它们都存在无法解决的问题,这也是正是我们推出Tinker的原因。下面我们来看一张图,就可以知道Tinker的强大之处。Tinker的已知问题:
由于原理与系统限制,Tinker有以下已知问题:
Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件
由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
在Android N上,补丁对应用启动时间有轻微的影响;
不支持部分三星android-21机型,加载补丁时会主动抛出"TinkerRuntimeException:checkDexInstall failed";
对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。
上面的介绍摘选自Tinker官方文档,如需要更加详细文档,访问:Tinker官方地址。在对Tinker有个简单了解后,下面我们就开始在项目中一步步集成Tinker了。
- Gradle文件配置
(1)在项目根目录下的gradle.properties文件中添加Tinker版本属性(对tinker的版本信息统一管理,方便后续版本升级维护)如下所示:
TINKER_VERSION = 1.9.1
(2)在项目根目录下的build.gradle文件中添加“tinker-gradle-plugin ”。
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
classpath ("com.tencent.tinker:tinker-patch-gradle-plugin:${TINKER_VERSION}")
}
(3)在app目录下的build.gradle文件中添加依赖:
//tinker自定义注解库,生成application时使用 只参与编译,不参与打包
provided("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}") { changing = true }
//tinker核心sdk库 参与编译与打包
compile("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}") { changing = true }
//支持分包操作
compile 'com.android.support:multidex:1.0.1'
(4)根据Tinker官方Demo在app目录下build.gradle文件中配置信息:
def bakPath = file("${buildDir}/bakApk/") //指定基准文件存放位置
ext {
tinkerEnable = true
tinkerOldApkPath = "${bakPath}/"
tinkerID = "1.0"
tinkerApplyMappingPath = "${bakPath}/"
tinkerApplyResourcePath = "${bakPath}/"
}
def buildWithTinker() {
return ext.tinkerEnable
}
def getOldApkPath() {
return ext.tinkerOldApkPath
}
def getApplyMappingPath() {
return ext.tinkerApplyMappingPath
}
def getApplyResourceMappingPath() {
return ext.tinkerApplyResourcePath
}
def getTinkerIdValue() {
return ext.tinkerID
}
def getTinkerBuildFlavorDirectory(){
return ext.tinkerBuildFlavorDirectory
}
if (buildWithTinker()) {
//启用tinker
apply plugin: 'com.tencent.tinker.patch'
//所有tinker相关的参数配置
tinkerPatch {
oldApk = getOldApkPath() //指定old apk文件路径
ignoreWarning = false //不忽略tinker的警告,有警告则中止patch文件的生成
useSign = true //强制patch文件也使用签名
tinkerEnable = buildWithTinker(); //指定是否启用tinker
buildConfig {
applyMapping = getApplyMappingPath() //指定old apk打包时所使用的混淆文件
applyResourceMapping = getApplyResourceMappingPath() //指定old apk的资源文件
tinkerId = getTinkerIdValue() //指定TinkerID
keepDexApply = false
}
dex {
dexMode = "jar" //jar、raw
pattern = ["classes*.dex", "assets/secondary-dex-?.jar"] //指定dex文件目录
loader = ["androidjian.tinker.MyTinkerApplication"] //指定加载patch文件时用到的类
}
lib {
pattern = ["libs/*/*.so"]
}
res {
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
//指定tinker可以修改的所有资源路径
ignoreChange = ["assets/sample_meta.txt"] //指定不受影响的资源路径
largeModSize = 100 //资源修改大小默认值
}
packageConfig {
configField("patchMessage", "fix the 1.0 version's bugs")
configField("patchVersion", "1.0")
}
}
//判断当前是否配置多渠道
List flavors = new ArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
/**
* 复制基准包和其它必须文件到指定目录
*/
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")
}
}
}
}
}
}
}
- 创建TinkerManager类,对所有Tinker相关的API进行封装,(减少对项目的侵入,方便后续维护)。
public class TinkerManager {
//标记是否安装过Tinker
private static boolean isInstalled = false;
private static ApplicationLike mAppLike;
/**
* 完成tinker的初始化
* @param applicationLike
*/
public static void installTinker(ApplicationLike applicationLike) {
mAppLike = applicationLike;
if (isInstalled){
return;
}
//完成Tinker的初始化
TinkerInstaller.install(mAppLike);
isInstalled = true;
}
/**
* 完成patch文件的加载
* @param path
*/
public static void loadPatch(String path){
if (Tinker.isTinkerInstalled()){
TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(),path);
}
}
private static Context getApplicationContext(){
if (mAppLike != null){
return mAppLike.getApplication().getApplicationContext();
}
return null;
}
}
- 新建TinkerApplicationLike 类,继承自DefaultApplicationLike:
@DefaultLifeCycle(application = ".MyTinkerApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class TinkerApplicationLike extends DefaultApplicationLike{
public TinkerApplicationLike(Application application, int tinkerFlags,
boolean tinkerLoadVerifyFlag,
long applicationStartElapsedTime,
long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime,
applicationStartMillisTime, tinkerResultIntent);
}
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
MultiDex.install(base);
TinkerManager.installTinker(this);
}
}
注意这里我们自定义的application类的名称为MyTinkerApplication,记得在manifest清单文件中的application节点下添加name属性为MyTinkerApplication。然后我们还需要重写onBaseContextAttached()方法,在该方法中完成Tinker的初始化操作。
到此为止,tinker的集成操作已经完成了。接下来我们来验证一下。
1.首先我们要生成基准APK:
- 配置基准文件信息
在1中我们看到tinker为我们生成了基准文件信息,接下来我们需要在build.gradle文件中配置基准文件信息,为后续生成patch文件做准备。操作如下:
ext {
tinkerEnable = true
tinkerOldApkPath = "${bakPath}/app-release-0722-20-04-03.apk"
tinkerID = "1.0"
tinkerApplyMappingPath = "${bakPath}/app-release-0722-20-04-03-mapping.txt"
tinkerApplyResourcePath = "${bakPath}/app-release-0722-20-04-03-R.txt"
}
改动代码,模拟项目更新或者bug修复
由tinker的官方文档可知,tinker支持动态下发代码、So库以及资源,它的功能不仅仅限于修改bug操作。old APK中只放置了一个按钮,负责加载指定目录下的patch文件。这里我在布局文件中新增了一个textView,text为“hello tinker”。-
生成patch补丁文件
我们打开Android Studio右侧面板的Gradle,可以看到如下:
patch_signed.apk就是我们最终要用到的补丁文件。
-
将patch补丁文件push到手机指定目录,进行验证。
在patch补丁文件加载之前,界面中只有一个按钮,如下图:
可以看到tinker已经成功集成到我们的项目中了。