由于原理与系统限制,Tinker有以下已知问题
- Tinker不支持修改AndroidManifest.xml,Tinker不支持新增四大组件(1.9.0支持新增非export的Activity);
- 由于Google Play的开发者条款限制,不建议在GP渠道动态更新代码;
- 在Android N上,补丁对应用启动时间有轻微的影响;
- 不支持部分三星android-21机型,加载补丁时会主动抛出”TinkerRuntimeException:checkDexInstall failed”;
- 对于资源替换,不支持修改remoteView。例如transition动画,notification icon以及桌面图标。
接入Tinker的几种方式
TinkerPatch 平台
提供了补丁后台托管,版本管理,保证传输安全等功能,让你无需搭建一个后台,无需关心部署操作,只需引入一个 SDK 即可立即使用 Tinker。
第三方平台
如TinkerPatch平台 和Bugly热更新功能
这种方式对Application进行了反射,是有风险
反射失败的情况,我们会自动回退到代理 Application
生命周期模式,防止因为反射失败而造成应用无法启动的问题。
自己后台管理patch包
主要介绍这种
1. 命令行接入
2. gradle接入
gradle接入
介绍:通过gradle脚本生成patch包
然后让 old.apk(对应版本打的修护包)加载patch包
第一步,引入Tinker插件和依赖
工程根目录下的build.gradle的dependencies中
buildscript {
dependencies {
classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.2')
}
}
主module中的build.gradle文件里
// tinker config
apply from: rootProject.file('tinker_support.gradle')
android {
//依赖配置
dependencies {
provided('com.tencent.tinker:tinker-android-anno::1.9.2')
//tinker的核心库
compile('com.tencent.tinker:tinker-android-lib::1.9.2')
}
}
创建tinker_support.gradle 在最外层的build.gradle
tinker_support.gradle
//基准apk包的备份路径,这里仅作备份用,
//每次构建apk时会将生成的apk文件、mapping和R文件自动拷贝一份到这个目录下去
def bakPath = file("${buildDir}/bakApk/")
/**
* you can use assembleRelease to build you base apk
* use tinkerPatchRelease -POLD_APK= -PAPPLY_MAPPING= -PAPPLY_RESOURCE= to build patch
* add apk from the build/bakApk
*/
ext {
//for some reason, you may want to ignore tinkerBuild, such as instant run debug build?
// /** 可以在debug的时候关闭 tinkerPatch **/
tinkerEnabled = true
//构建时需要在此配置基准包的filename
tinkerBaseApkFileName = getTinkerBaseApkFileName("upwallet-debug-0118-10-11-12.apk")
//proguard mapping file to build patch apk
tinkerMappingFileName = tinkerBaseApkFileName.substring(0, tinkerBaseApkFileName.length() - 4) + "-mapping.txt"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerSymbolFileName = tinkerBaseApkFileName.substring(0, tinkerBaseApkFileName.length() - 4) + "-R.txt"
//old apk file to build patch apk
tinkerOldApkPath = "${bakPath}/upwallet-debug-0118-10-11-12.apk"
//resource R.txt to build patch apk, must input if there is resource changed
tinkerApplyResourcePath = "${bakPath}/"+tinkerSymbolFileName
//proguard mapping file to build patch apk
tinkerApplyMappingPath = "${bakPath}/"+tinkerMappingFileName
//only use for build all flavor, if not, just ignore this field
tinkerBuildFlavorDirectory = "${bakPath}/app-1018-17-32-47"
}
//构建补丁时获取基准apk包的文件名
def getTinkerBaseApkFileName(def defaultName) {
return hasProperty("TINKER_BASE_APK_NAME") ? TINKER_BASE_APK_NAME : defaultName
}
def buildWithTinker() {
return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}
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
}
/**
* Tinker插件配置信息
*/
if (buildWithTinker()) {
// 依赖tinker插件
apply plugin: 'com.tencent.tinker.patch'
// 全局信息相关配置项
tinkerPatch {
/**
* necessary,default 'null'
* the old apk path, use to diff with the new apk to build
* add apk from the build/bakApk
* 必选, 基准包路径
*/
oldApk = getOldApkPath()
/**
* optional,default 'false' 可选,默认false
* 如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。
* 因为这些情况可能会导致编译出来的patch包带来风险:
* case 1: minSdkVersion小于14,但是dexMode的值为"raw";
* case 2: 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...);
* case 3: 定义在dex.loader用于加载补丁的类不在main dex中;
* case 4: 定义在dex.loader用于加载补丁的类出现修改;
* case 5: resources.arsc改变,但没有使用applyResourceMapping编译。
*/
ignoreWarning = false
/**
* optional,default 'true'
* 可选,默认true, 验证基准apk和patch签名是否一致
* 运行过程中需要验证基准apk包与补丁包的签名是否一致,是否需要签名。
* whether sign the patch file
* if not, you must do yourself. otherwise it can't check success during the patch loading
* we will use the sign config with your build type
*/
useSign = true
/**
* optional,default 'true'
* whether use tinker to build
*/
tinkerEnable = buildWithTinker()
// 用于生成补丁包中的'package_meta.txt'文件
buildConfig {
tinkerId = BUILD_TOOLS_VERSION // 必选,
/**
* if keepDexApply is true, class in which dex refer to the old apk.
* open this can reduce the dex diff file size.
* 如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。
* 若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。
*/
keepDexApply = false
/**
* optional,default 'null'
* 可选参数;在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,
* 这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常。
*/
applyResourceMapping = getApplyResourceMappingPath()
/**
* optional,default 'null'
* 可选参数;在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,
* 从而减少补丁包的大小。这个只是推荐设置,
* 不设置applyMapping也不会影响任何的assemble编译。
*/
applyMapping = getApplyMappingPath()
/**
* optional, default 'false'
* 是否使用加固模式,仅仅将变更的类合成补丁。
* 注意,这种模式仅仅可以用于加固应用中。
*/
isProtectedApp = false
/**
* optional, default 'false'
* Whether tinker should support component hotplug (add new component dynamically).
* If this attribute is true, the component added in new apk will be available after
* patch is successfully loaded. Otherwise an error would be announced when generating patch
* on compile-time.
*
* <b>Notice that currently this feature is incubating and only support NON-EXPORTED Activity</b>
*/
supportHotplugComponent = false
}
dex {
/**
* 只能是'raw'或者'jar'。
* 对于'raw'模式,我们将会保持输入dex的格式。
* 对于'jar'模式,我们将会把输入dex重新压缩封装到jar。
* 如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,但是验证md5时比'raw'模式耗时。
* 默认我们并不会去校验md5,一般情况下选择jar模式即可。
*/
dexMode = "jar"
/**
* necessary,default '[]'
* 需要处理dex路径,支持*、?通配符,必须使用'/'分割。
* 路径是相对安装包的,例如assets/...
*/
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
/**
* necessary,default '[]'
* 这一项非常重要,它定义了哪些类在加载补丁包的时候会用到。
* 这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。
* 这里需要定义的类有:
* 1. 你自己定义的Application类;
* 2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;
* 3. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
* 4. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。
* 这里需要注意的是,这些类的直接引用类也需要加入到loader中。
* 或者你需要将这个类变成非preverify。
* 5. 使用1.7.6版本之后版本,参数1、2会自动填写。
*
*/
loader = [
//Tinker库中用于加载补丁包的部分类
"tinker.sample.android.app.BaseBuildInfo",
]
}
/**
* lib相关的配置项
*/
lib {
/**
* optional,default '[]'
* 需要处理lib路径,支持*、?通配符,必须使用'/'分割。
* 与dex.pattern一致, 路径是相对安装包的,例如assets/...
*/
pattern = ["lib/*/*.so"]
}
/**
* res相关的配置项
*/
res {
/**
* optional,default '[]'
* 需要处理res路径,支持*、?通配符,必须使用'/'分割。
* 与dex.pattern一致, 路径是相对安装包的,例如assets/...,
* 务必注意的是,只有满足pattern的资源才会放到合成后的资源包。
*/
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
/**
* optional,default '[]'
* 支持*、?通配符,必须使用'/'分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。
* 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。
*/
ignoreChange = ["assets/sample_meta.txt"]
/**
* default 100kb
* 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。
* 这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb
*/
largeModSize = 100
}
/**
* 用于生成补丁包中的'package_meta.txt'文件
*/
packageConfig {
/**
* optional,default 'TINKER_ID, TINKER_ID_VALUE' 'NEW_TINKER_ID, NEW_TINKER_ID_VALUE'
* configField("key", "value"), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。
* 在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。
* 但是建议直接通过修改代码来实现,例如BuildConfig。
*/
configField("patchMessage", "tinker is sample to use")
/**
* just a sample case, you can use such as sdkVersion, brand, channel...
* you can parse it in the SamplePatchListener.
* Then you can use patch conditional!
*/
configField("platform", "all")
/**
* 配置patch补丁版本
*/
configField("patchVersion", "1.0")
}
/**
* 7zip路径配置项,执行前提是useSign为true
*/
sevenZip {
/**
* optional,default '7za'
* the 7zip artifact path, it will use the right 7za with your platform
*/
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
/**
* optional,default '7za'
* you can specify the 7za path yourself, it will overwrite the zipArtifact value
*/
// path = "/usr/local/bin/7za"
}
}
List<String> flavors = new ArrayList<>();
project.android.productFlavors.each { flavor ->
flavors.add(flavor.name)
}
boolean hasFlavors = flavors.size() > 0
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
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.first().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"
}
}
}
}
}
}
代码application AndroidManifest.xml
改造自己的Application,详细可参考自定义Application类
我们在AndroidManifest.xml会自己定义一个application文件 如图:
替换成如图(名字可以自己取,位置最好和以前application同级):
虽然我们这么写了,但是实际上Application会在编译期生成
意思就是不需要去创建一个类,build它会自己生成
如果报红,也可以build下
AndroidManifest还需要添加
<meta-data
android:name="TINKER_ID"
android:value="最好是自己的版本号" />
<!--tinker 根据情况是否需要-->
<service
android:name="com.tinker.service.SampleResultService"
android:exported="false"/>
以前Application修改为
//tinker推荐下面的写法
//UPTinkerApplication 是application
@DefaultLifeCycle(application = "com.xxxxx.base.UPTinkerApplication",
flags = ShareConstants.TINKER_ENABLE_ALL,
loadVerifyFlag = false)
public class UPApplication extends ApplicationLike {
public UPApplication(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
/**
* install multiDex before install tinker
* so we don't need to put the tinker lib classes in the main dex
*
* @param base
*/
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
@Override
public void onBaseContextAttached(Context base) {
super.onBaseContextAttached(base);
//you must install multiDex whatever tinker is installed!
MultiDex.install(base);
TinkerManager.setTinkerApplicationLike(this);
TinkerManager.initFastCrashProtect();
//should set before tinker is installed
TinkerManager.setUpgradeRetryEnable(true);
//optional set logIml, or you can use default debug log
TinkerInstaller.setLogIml(new MyLogImp());
//installTinker after load multiDex
//or you can put com.tencent.tinker.** to main dex
TinkerManager.installTinker(this);
Tinker tinker = Tinker.with(getApplication());
}
}
onBaseContextAttached 配置是根据 Tinker提供的Demo编入
当然也可以不按照Demo写
引入文件如下 不做分析 UPTinkerUtils自己写的工具类