Tinker项目(点击进入)
Tinker是微信官方的Android热补丁解决方案,它支持动态下发代码。.so库以及资源库,让应用可以在不须要重复安装的状况下实现更新,固然也可使用Tinker来更新你的插件。css
在接入Tinker以前咱们先对Tinker的结构了解一下java
Tinker主要包括一下几个部分:
1.gradle编译插件:tinker-patch-gradle-plugin。
2.核心SDK库:tinker-android-lib。
3.非gradle编译用户的命令行版本:tinker-patch-cil.jar。android
Tinker的已知问题:
1.Tinker不支持修改AndroidMainfest.xml,Tinker不支持新增四大组件。
2.因为Google Pay的开发者条款限制,不建议在GP渠道动态更新代码。
3.在Android N上,补丁对应用启动时有轻微的影响。
4.不支持部分三星android-21机型,加载补丁时会主动抛出“TinkerRuntimeException:checkDexInstall failed”异常。
5.因为各个厂商加固实现并不一致,在1.7.6之后的版本,Tinker不在支持加固的动态更新。
6.对于资源替换,不支持修改remoteView,例如transition动画,notification icon以及桌面图标。git
Tinker的修复方案跟Hotfix的修复方案大同小异,都是在两个apk包上做比较而后生成patch。下面对Tinker进行接入。github
在工程目录下的build.gradle中添加依赖库
buildscript { repositories { jcenter() }
dependencies { classpath 'com.android.tools.build:gradle:2.2.3' // 编译插件tinker-patch-gradle-plugin classpath 'com.tencent.tinker:tinker-patch-gradle-plugin:1.7.7' }
}
allprojects { repositories { jcenter() }
}
task clean(type: Delete) { delete rootProject.buildDir }
在工程app目录下的build.gradle中添加依赖库
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
......
//可选,用于生成application类
provided('com.tencent.tinker:tinker-android-anno:1.7.7')
//tinker的核心库
compile('com.tencent.tinker:tinker-android-lib:1.7.7')
}
API引入
通常咱们都是在Application中onCreate()中作初始化和加载patch,不过Tinker推荐以下写法。由于程序启动时会加载默认的Application类,这致使咱们补丁包是没法对它作修改了。为了规避这个问题Tinker经过代码框架的方式来避免,这也是为了尽可能少的去反射,提高框架的兼容性。web
@DefaultLifeCycle(
application = ".AppContext", flags = ShareConstants.TINKER_ENABLE_ALL
)
public class AppContextLike extends ApplicationLike {
public AppContextLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) {
super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent);
}
@Override
public void onCreate() {
super.onCreate();
TinkerInstaller.install(this);
}
}
代码中AppContextLike继承了ApplicationLike,而ApplicationLike并不是集成Application,而是相似于Application的一个类。Tinker建议编写一个ApplicationLike的子类,能够当作Application使用,注意顶部的注解:@DefaultLifeCycle,其application属性,会在编译期生成一个.AppContext类。因此咱们在AndroidManifest.xml中的application标签下这样写:算法
<application
android:name=".AppContext"
......
</application>
写完后会报红,此时只须要Build下便可解决报红。Application配置就到此结束。接下来生成patch文件。由于patch文件是写入到SDCrad的,因此咱们须要在AndroidManifest中添加以下权限(注: 6.0及已上系统请动态设置权限或者手动在设置中为项目设置):api
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
Tinker须要在AndroidManifest.xml中指定TINKER_ID微信
<meta-data
android:name="TINKER_ID"
android:value="tinker_id_100" />
Patch生成
patch生成官方提供了两种接入方式:app
1.基于命令行的方式。
2.gradle编译的方式。
1.gradle编译生成patch
微信Tinker的gradle配置也很简单,先来浏览一下Tinker接入指南,点击进入查看,对使用gradle配置的参数了解一下,接下来附上一个相对比较完整的gradle配置。
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
compileOptions {
sourceCompatibility getJavaVersion()
targetCompatibility getJavaVersion()
}
// Tinker推荐模式
dexOptions {
jumboMode = true
}
// 关闭aapt对png优化
aaptOptions {
cruncherEnabled false
}
signingConfigs {
try {
config {
keyAlias 'testres'
keyPassword 'testres'
storeFile file('./keystore/release.keystore')
storePassword 'testres'
}
} catch (ex) {
throw new InvalidUserDataException(ex.toString())
}
}
defaultConfig {
applicationId "com.tinker.app"
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0.0"
// 使用multiDex库
multiDexEnabled true
// 设置签名
signingConfig signingConfigs.config
manifestPlaceholders = [TINKER_ID: "${getTinkerIdValue()}"]
buildConfigField "String", "MESSAGE", "\"I am the base apk\""
buildConfigField "String", "CLIENTVERSION", "\"${getTinkerIdValue()}\""
buildConfigField "String", "PLATFORM", "\"all\""
}
buildTypes {
release {
minifyEnabled false
signingConfig signingConfigs.config
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
minifyEnabled false
signingConfig signingConfigs.config
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile 'com.android.support:appcompat-v7:25.1.0'
// 依赖multiDex库
compile 'com.android.support:multidex:1.0.1'
//可选,用于生成application类
provided ("com.tencent.tinker:tinker-android-anno:${TINKER_VERSION}"){changing = true}
//Tinker的核心库
compile ("com.tencent.tinker:tinker-android-lib:${TINKER_VERSION}"){changing = true}
}
// 指定JDK版本
def getJavaVersion() {
return JavaVersion.VERSION_1_7
}
def bakPath = file("${buildDir}/bakApk/")
/** * ext相关配置 */
ext {
tinkerEnabled = true
// 基础版本apk
tinkerOldApkPath = "${bakPath}/app-debug-20170217-18-51-30.apk"
// 未开启混淆的话mapping能够忽略,若是开启混淆mapping要保持一致。
tinkerApplyMappingPath = "${bakPath}/"
// 与基础版本一块儿生成的R.text文件
tinkerApplyResourcePath = "${bakPath}/app-debug-20170217-18-51-30-R.txt"
// 只用于构建全部的Build,若是不是,此处可忽略。
tinkerBuildFlavorDirectory = "${bakPath}/"
}
// 基础APK的位置
def getOldApkPath() {
return hasProperty("OLD_APK") ? OLD_APK : ext.tinkerOldApkPath
}
// Mapping的位置
def getApplyMappingPath() {
return hasProperty("APPLY_MAPPING") ? APPLY_MAPPING : ext.tinkerApplyMappingPath
}
// ResourceMapping的位置
def getApplyResourceMappingPath() {
return hasProperty("APPLY_RESOURCE") ? APPLY_RESOURCE : ext.tinkerApplyResourcePath
}
// 用来获取TinkerId(当前版本号就是TinkerId)
def getTinkerIdValue() {
return android.defaultConfig.versionName
}
def buildWithTinker() {
return hasProperty("TINKER_ENABLE") ? TINKER_ENABLE : ext.tinkerEnabled
}
def getTinkerBuildFlavorDirectory() {
return ext.tinkerBuildFlavorDirectory
}
if (buildWithTinker()) {
// Tinker插件
apply plugin: 'com.tencent.tinker.patch'
/** * 全局信息相关配置 */
tinkerPatch {
// 基准apk包的路径,必须输入,不然会报错。
oldApk = getOldApkPath()
/** * 若是出现如下的状况,而且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
/** * 运行过程当中须要验证基准apk包与补丁包的签名是否一致,是否须要签名。 */
useSign = true
/** * optional,default 'true' * whether use tinker to build */
tinkerEnable = buildWithTinker()
/** * 编译相关的配置项 */
buildConfig {
/** * 可选参数;在编译新的apk时候,咱们但愿经过保持旧apk的proguard混淆方式,从而减小补丁包的大小。 * 这个只是推荐设置,不设置applyMapping也不会影响任何的assemble编译。 */
applyMapping = getApplyMappingPath()
/** * 可选参数;在编译新的apk时候,咱们但愿经过旧apk的R.txt文件保持ResId的分配。 * 这样不只能够减小补丁包的大小,同时也避免因为ResId改变致使remote view异常。 */
applyResourceMapping = getApplyResourceMappingPath()
/** * 在运行过程当中,咱们须要验证基准apk包的tinkerId是否等于补丁包的tinkerId。 * 这个是决定补丁包能运行在哪些基准包上面,通常来讲咱们可使用git版本号、versionName等等。 */
tinkerId = getTinkerIdValue()
/** * 若是咱们有多个dex,编译补丁时可能会因为类的移动致使变动增多。 * 若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。 */
keepDexApply = false
}
/** * dex相关的配置项 */
dex {
/** * 只能是'raw'或者'jar'。 * 对于'raw'模式,咱们将会保持输入dex的格式。 * 对于'jar'模式,咱们将会把输入dex从新压缩封装到jar。 * 若是你的minSdkVersion小于14,你必须选择‘jar’模式,并且它更省存储空间,可是验证md5时比'raw'模式耗时。 * 默认咱们并不会去校验md5,通常状况下选择jar模式便可。 */
dexMode = "jar"
/** * 须要处理dex路径,支持*、?通配符,必须使用'/'分割。路径是相对安装包的,例如assets/... */
pattern = ["classes*.dex",
"assets/secondary-dex-?.jar"]
/** * 这一项很是重要,它定义了哪些类在加载补丁包的时候会用到。 * 这些类是经过Tinker没法修改的类,也是必定要放在main dex的类。 * 这里须要定义的类有: * 1. 你本身定义的Application类; * 2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*; * 3. 若是你自定义了TinkerLoader,须要将它以及它引用的全部类也加入loader中; * 4. 其余一些你不但愿被更改的类,例如Sample中的BaseBuildInfo类。 * 这里须要注意的是,这些类的直接引用类也须要加入到loader中。 * 或者你须要将这个类变成非preverify。 * 5. 使用1.7.6版本以后版本,参数一、2会自动填写。 * */
loader = [
// Tinker库中用于加载补丁包的部分类
"com.tencent.tinker.loader.*",
// 本身定义的Application类;
"com.tinker.app.AppContext",
//use sample, let BaseBuildInfo unchangeable with tinker
"tinker.sample.android.app.BaseBuildInfo"
]
}
/** * lib相关的配置项 */
lib {
/** * 须要处理lib路径,支持*、?通配符,必须使用'/'分割。 * 与dex.pattern一致, 路径是相对安装包的,例如assets/... */
pattern = ["lib/*/*.so"]
}
/** * res相关的配置项 */
res {
/** * 须要处理res路径,支持*、?通配符,必须使用'/'分割。 * 与dex.pattern一致, 路径是相对安装包的,例如assets/..., * 务必注意的是,只有知足pattern的资源才会放到合成后的资源包。 */
pattern = ["res/*", "assets/*", "resources.arsc", "AndroidManifest.xml"]
/** * 支持*、?通配符,必须使用'/'分割。若知足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。 * 最极端的状况,ignoreChange与上面的pattern一致,即会彻底忽略全部资源的修改。 */
ignoreChange = ["assets/sample_meta.txt"]
/** * 对于修改的资源,若是大于largeModSize,咱们将使用bsdiff算法。 * 这能够下降补丁包的大小,可是会增长合成时的复杂度。默认大小为100kb */
largeModSize = 100
}
/** * 用于生成补丁包中的'package_meta.txt'文件 */
packageConfig {
/** * 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.0")
}
/** * 7zip路径配置项,执行前提是useSign为true */
sevenZip {
/** * 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性得到对应的7za运行文件,推荐使用。 */
zipArtifact = "com.tencent.mm:SevenZip:1.1.10"
/** * 系统中的7za路径,例如"/usr/local/bin/7za"。path设置会覆盖zipArtifact,若都不设置,将直接使用7za去尝试。 */
// 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("yyyyMMdd-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"
}
}
}
}
}
}
gradle配置就到此结束了,要注意的地方有如下几点:
1.ext相关配置,示例中有完整描述。
2.Tinker插件apply plugin: 'com.tencent.tinker.patch'
3.全局信息相关配置tinkerPatch
配置完这些东西之后就能够调用tinkerPatch命令生成patch补丁文件。tinkerPatch有Debug和Release两种模式,由于是案例,因此就使用tinkerPatchDebug命令。
注意:调用tinkerPatchDebug命令以前须要修改ext相关配置,ext相关配置已基准apk包为准。
到此微信Tinker热修复gradle配置结束。
案例地址:https://github.com/mengjingbo/TinkerApp
本文分享 CSDN - 秦川小将。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。