您的位置:首页 > 移动开发 > Android开发

Android 热修复方案Tinker(六) Gradle插件实现

2016-12-29 11:53 477 查看
基于Tinker V1.7.5

Android 热修复方案Tinker(一) Application改造

Android 热修复方案Tinker(二) 补丁加载流程

Android 热修复方案Tinker(三) Dex补丁加载

Android 热修复方案Tinker(四) 资源补丁加载

Android 热修复方案Tinker(五) SO补丁加载

Android 热修复方案Tinker(六) Gradle插件实现

Android 热修复方案Tinker(七) 插桩实现

带注释的源码

这篇文章主要分析一下Tinker中gradle插件的设计以及各个任务的职能.Gradle插件工作流程的简单实现在Android Gradle 插件编写文章中有讲过,这里就不复述了.下图是Tinker Gradle插件的类图结构.点击查看大图



Gradle

Gradle里需要配置插件中自定义的扩展.扩展block层级,属性和含义结合Tinker的文档如下.

tinkerPatch

buildConfig

dex

lib

res

packageConfig

sevenZip

tinkerPatch

全局信息相关的配置项

参数默认值描述
oldApknull基准apk包的路径,必须输入,否则会报错。
ignoreWarningfalse如果出现以下的情况,并且ignoreWarning为false,Tinker将中断编译。因为这些情况可能会导致编译出来的patch包带来风险:
1. minSdkVersion小于14,但是
dexMode
的值为”raw”;
2. 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver…);
3. 定义在dex.loader用于加载补丁的类不在main dex中;
4. 定义在dex.loader用于加载补丁的类出现修改;
5. resources.arsc改变,但没有使用applyResourceMapping编译。
useSigntrue在运行过程中,Tinker需要验证基准apk包与补丁包的签名是否一致,Tinker是否需要为你签名。
buildConfig

编译相关的配置项

参数默认值描述
applyMappingnull可选参数;在编译新的apk时候,Tinker通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。这个只是推荐的,但
设置applyMapping会影响任何的assemble编译
applyResourceMappingnull可选参数;在编译新的apk时候,Tinker通过旧apk的
R.txt
文件保持ResId的分配,这样不仅
可以减少补丁包的大小
,同时也
避免由于ResId改变导致remote view异常
。
tinkerIdnull在运行过程中,Tinker需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。
dex

dex相关的配置项

参数默认值描述
dexModejar只能是’raw’或者’jar’。
对于’raw’模式,Tinker将会保持输入dex的格式。
对于’jar’模式,Tinker会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,但是验证md5时比’raw’模式耗时()。
usePreGeneratedPatchDexflase是否提前生成dex,而非合成的方式。这套方案即回退成Qzone的方案,对于需要使用
加固或者多flavor打包(建议使用其他方式生成渠道包)的用户
可使用。但是这套方案需要插桩,会造成
Dalvik下性能损耗以及Art补丁包可能过大的问题,务必谨慎使用
pattern[]需要处理dex路径,支持*、?通配符,必须使用’/’分割。路径是相对安装包的,例如/assets/…
loader[]这一项非常重要,它定义了哪些类在加载补丁包的时候会用到。这些类是通过Tinker无法修改的类,也是一定要放在main dex的类。
这里需要定义的类有:
1. 你自己定义的Application类;
2. Tinker库中用于加载补丁包的部分类,即com.tencent.tinker.loader.*;
3. 如果你自定义了TinkerLoader,需要将它以及它引用的所有类也加入loader中;
4. 其他一些你不希望被更改的类,例如Sample中的BaseBuildInfo类。这里需要注意的是,这些类的直接引用类也需要加入到loader中。或者你需要将这个类变成非preverify。
lib

lib相关的配置项

参数默认值描述
pattern[]需要处理lib路径,支持*、?通配符,必须使用’/’分割。与dex.pattern一致, 路径是相对安装包的,例如/assets/…
res

res相关的配置项

参数默认值描述
pattern[]需要处理res路径,支持*、?通配符,必须使用’/’分割。与dex.pattern一致, 路径是相对安装包的,例如/assets/…,
务必注意的是,只有满足pattern的资源才会放到合成后的资源包。
ignoreChange[]支持*、?通配符,必须使用’/’分割。若满足ignoreChange的pattern,在编译时会忽略该文件的新增、删除与修改。 最极端的情况,ignoreChange与上面的pattern一致,即会完全忽略所有资源的修改。
largeModSize100对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb
packageConfig

用于生成补丁包中的’package_meta.txt’文件

参数默认值描述
configFieldTINKER_ID, NEW_TINKER_IDconfigField(“key”, “value”), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。但是建议直接通过修改代码来实现,例如BuildConfig。
sevenZip

7zip路径配置项,执行前提是useSign为true

参数默认值描述
zipArtifactnull例如”com.tencent.mm:SevenZip:1.1.10”,将自动根据机器属性获得对应的7za运行文件,推荐使用。
path7za系统中的7za路径,例如”/usr/local/bin/7za”。path设置会覆盖zipArtifact,若都不设置,将直接使用7za去尝试。

Extension

之前文章有介绍过,Extension中的属性和gralde的的配置是一一对应的.上面的gradle的扩展block一共有7个, 那么在插件中也要创建出7个Extension对象来映射对应的属性.

TinkerPatchExtension

Tinker全局配置的自定义扩展, 映射Gradle中
tinkerPatch
的属性配置, 并提供属性校验的共有方法, 主要效验
oldApk
属性是否有效并且指向的文件是否存在,否则抛出Gradle异常.

void checkParameter() {
if (oldApk == null) {
throw new GradleException("old apk is null, you must set the correct old apk value!")
}
File apk = new File(oldApk)
if (!apk.exists()) {
throw new GradleException("old apk ${oldApk} is not exist, you must set the correct old apk value!")
} else if (!apk.isFile()) {
throw new GradleException("old apk ${oldApk} is a directory, you must set the correct old apk value!")
}

}


TinkerBuildConfigExtension

编译相关配置项的自定义扩展, 映射Gradle中
buildConfig
的属性配置,并提供属性校验的共有方法, 主要效验
tinkerId
属性是否有效,否则抛出Gradle异常.

void checkParameter() {
if (tinkerId == null || tinkerId.isEmpty()) {
throw new GradleException("you must set your tinkerId to identify the base apk!")
}
}


TinkerDexExtension

dex相关配置项的自定义扩展, 映射Gradle中
dex
的属性配置,并提供校验
dexMode
属性是否为
raw | jar
方法,不在正常范围内就抛出Gradle异常.

void checkDexMode() {
if (!dexMode.equals("raw") && !dexMode.equals("jar")) {
throw new GradleException("dexMode can be only one of 'jar' or 'raw'!")
}
}


TinkerLibExtension

lib相关配置项的自定义扩展,映射Gradle中
lib
支持更新的路径集合.

TinkerResourceExtension

资源相关配置项的自定义扩展,映射Gradle中
res
的属性配置,并校验
largeModeSize
是否有效, 否则抛出Gradle异常.

void checkParameter() {
if (largeModSize <= 0) {
throw new GradleException("largeModSize must be larger than 0")
}
}


TinkerPackageConfigExtension

用于生成补丁包中的’package_meta.txt’文件,映射Gradle
packageConfig
中的配置属性, 并对外暴露访问这些map属性的方法.同时还提供了获取基准包manifest中meta的方法,但是这些方法在这个版本中并没有使用.

TinkerSevenZipExtension

7zip路径配置项, 映射Grade
sevenZip
中的属性.获取到以groupId:artifactId:version为格式拼装的
zipArtifact
,并在插件运行的过程中建立起来对该artifact的依赖, 并最终获取到配置依赖的运行文件路径.

Task

TinkerPatchSchemaTask

负责校验Extensions的参数和环境是否合法和补丁生成.这个Task牵扯的东西太多了,后面单独开一篇介绍.

TinkerManifestTask

建立Tinker的manifest任务,在manifestTask任务生成之后执行,并向android manifest文件的application层级中插入Tinker_ID,供app运行时使用.过程是先校验gradle中tinkerId是否设置.

String tinkerValue = project.extensions.tinkerPatch.buildConfig.tinkerId
if (tinkerValue == null || tinkerValue.isEmpty()) {
throw new GradleException('tinkerId is not set!!!')
}


再利用
XmlParser
解析manifest文件, 如果manifest文件的application层级下已经有TINKER_ID了就先删除掉.

def metaDataTags = application['meta-data']

// remove any old TINKER_ID elements
def tinkerId = metaDataTags.findAll {
it.attributes()[ns.name].equals(TINKER_ID)
}.each {
it.parent().remove(it)
}


并将gradle中配置的tinker_id插入到manifest中.

application.appendNode('meta-data', [(ns.name): TINKER_ID, (ns.value): tinkerValue])

// Write the manifest file
def printer = new XmlNodePrinter(new PrintWriter(manifestPath, "utf-8"))
printer.preserveWhitespace = true
printer.print(xml)


最后拷贝修改过的manifest文件到tinker的中间编译路径
build/intermediates/tinker_intermediates/
下.供开发者查看.

File manifestFile = new File(manifestPath)
if (manifestFile.exists()) {
FileOperation.copyFileUsingStream(manifestFile, project.file(MANIFEST_XML))
project.logger.error("tinker gen AndroidManifest.xml in ${MANIFEST_XML}")
}


TinkerResourceIdTask

该任务获取到
buildConfig.applyResourceMapping
配置的R文件中的映射, 并将它keep到补丁包生成的过程中.这个Task会跟TinkerPatchSchemaTask一起展开讲.

TinkerProguardConfigTask

如果开启了混淆,就会在gradle插件中构建出该任务,主要的作用是将tinker中默认的混淆信息和基准包的mapping信息加入混淆列表,这样就可以通过gradle配置自动帮开发者做一些类的混淆设置,并且可以通过applymapping的基准包的mapping文件达到在混淆上补丁包和基准包一致的目的.首先打开在编译路径下的混淆文件,为后面写入默认的keep规则做准备.文件的路径同样在
tinker_intermediates
下.

def file = project.file(PROGUARD_CONFIG_PATH)
project.logger.error("try update tinker proguard file with ${file}")

// Create the directory if it doesnt exist already
file.getParentFile().mkdirs()

// Write our recommended proguard settings to this file
FileWriter fr = new FileWriter(file.path)


如果gradle中配置的基准包mapping文件有效, 就将基准包的mapping文件apply进来.

String applyMappingFile = project.extensions.tinkerPatch.buildConfig.applyMapping

//write applymapping
if (shouldApplyMapping && FileOperation.isLegalFile(applyMappingFile)) {
project.logger.error("try add applymapping ${applyMappingFile} to build the package")
fr.write("-applymapping " + applyMappingFile)
fr.write("\n")
}


如果使用插桩模式, 则需要keep插桩涉及到的类和方法.

if (project.tinkerPatch.dex.usePreGeneratedPatchDex) {
def additionalKeptRules =
"-keep class ${AuxiliaryClassInjector.NOT_EXISTS_CLASSNAME} { \n" +
'    *; \n' +
'}\n' +
'\n' +
'-keepclassmembers class * { \n' +
'    <init>(...); \n' +
'    static void <clinit>(...); \n' +
'}\n'
fr.write(additionalKeptRules)
fr.write('\n')
}


将dex.loader中配置的类在混淆的时候也keep起来.

Iterable<String> loader = project.extensions.tinkerPatch.dex.loader
for (String pattern : loader) {
if (pattern.endsWith("*") && !pattern.endsWith("**")) {
pattern += "*"
}
fr.write("-keep class " + pattern)
fr.write("\n")
}
fr.close()


最终将上面拼装起来的混淆文件添加进混淆文件列表中使其生效.

applicationVariant.getBuildType().buildType.proguardFiles(file)
def files = applicationVariant.getBuildType().buildType.getProguardFiles()
project.logger.error("now proguard files is ${files}")


TinkerMultidexConfigTask

如果开启了multiDex 会在编译中根据gradle的配置和默认配置生成出要keep在main dex中的proguard信息文件,然后copy出这个文件,方便开发者使用
multiDexKeepProguard
进行配置.首先打开文件并写入默认配置.文件路径也在
tinker_intermediates
下.

def file = project.file(MULTIDEX_CONFIG_PATH)
project.logger.error("try update tinker multidex keep proguard file with ${file}")

// Create the directory if it doesn't exist already
file.getParentFile().mkdirs()

// Write our recommended proguard settings to this file
FileWriter fr = new FileWriter(file.path)

fr.write(MULTIDEX_CONFIG_SETTINGS)
fr.write("\n")


将dex.loader中配置的class也keep进main dex.写完文件之后开发者就可以将整个文件配置起来.

Iterable<String> loader = project.extensions.tinkerPatch.dex.loader
for (String pattern : loader) {
if (pattern.endsWith("*")) {
if (!pattern.endsWith("**")) {
pattern += "*"
}
}
fr.write("-keep class " + pattern + " {\n" +
"    *;\n" +
"}\n")
fr.write("\n")
}
fr.close()


Plugin

上面讲了用于接收和校验gradle扩展块属性的Extension和用于处理各个不同任务的task.而Plugin对象既是整个Gradle插件的入口又可以看成是Extension跟task的链接器.

构建Extension对象

最先做的就是构建出与gradle扩展相对应的7个Extension对象.

project.extensions.create('tinkerPatch', TinkerPatchExtension)

project.tinkerPatch.extensions.create('buildConfig', TinkerBuildConfigExtension, project)

project.tinkerPatch.extensions.create('dex', TinkerDexExtension, project)
project.tinkerPatch.extensions.create('lib', TinkerLibExtension)
project.tinkerPatch.extensions.create('res', TinkerResourceExtension)
project.tinkerPatch.extensions.create('packageConfig', TinkerPackageConfigExtension, project)
project.tinkerPatch.extensions.create('sevenZip', TinkerSevenZipExtension, project)


验证和配置默认android gradle属性

首先验证插件运行的gradle是不是application,不是的话直接crash掉.

if (!project.plugins.hasPlugin('com.android.application')) {
throw new GradleException('generateTinkerApk: Android Application plugin required')
}


再通过插件project拿到android gradle的Extension.去除一些打包时不需要的文件.

def android = project.extensions.android

//add the tinker anno resource to the package exclude option
android.packagingOptions.exclude("META-INF/services/javax.annotation.processing.Processor")
android.packagingOptions.exclude("TinkerAnnoApplication.tmpl")


接着修改android的dexOptions属性, 开启jumboMode并关闭preDexLibraries选项.如果开启preDexLibraries则可以脱离library编译出dex,用来辅助incremental编译. 开启了可能会影响到tinker生成补丁.

def configuration = project.tinkerPatch

//open jumboMode
android.dexOptions.jumboMode = true

//close preDexLibraries
try {
android.dexOptions.preDexLibraries = false
} catch (Throwable e) {
//no preDexLibraries field, just continue
}


注册插桩transform

由于Tinker在当前版本还支持回退qzone方案,所以肯定还是有插桩的动作,在gradle 1.5.0之前是根据preDex任务掌握时机使用asm或javasist做插桩,而gralde 1.5.0开始gradle就提供了Transform组件,可以用来做编译期间处理中间数据.Tinker的插桩就是基于Transform和asm实现的.具体的实现这里先不展开,后面会专门写一篇关于Tinker插桩的文档.

android.registerTransform(new AuxiliaryInjectTransform(project))


打印出Tinker的修改声明

通过gradle的logger打印出Tinker修改了哪些文件或者属性.

遍历variant 根据不同的variant名字创建tasks

如果开启了instant run直接crash掉

def instantRunTask = project.tasks.getByName("transformClassesWithInstantRunFor${variantName}")
if (instantRunTask) {
throw new GradleException(
"Tinker does not support instant run mode, please trigger build"
+ " by assemble${variantName} or disable instant run"
+ " in 'File->Settings...'."
)
}


根据当前variant构建出PatchSchemaTask任务, 用来初始化patch环境,验证Extension参数和生成补丁.

TinkerPatchSchemaTask tinkerPatchBuildTask = project.tasks.create("tinkerPatch${variantName}", TinkerPatchSchemaTask)
tinkerPatchBuildTask.dependsOn variant.assemble

tinkerPatchBuildTask.signConfig = variant.apkVariantData.variantConfiguration.signingConfig

variant.outputs.each { output ->
tinkerPatchBuildTask.buildApkPath = output.outputFile
File parentFile = output.outputFile
tinkerPatchBuildTask.outputFolder = "${parentFile.getParentFile().getParentFile().getAbsolutePath()}/" + TypedValue.PATH_DEFAULT_OUTPUT + "/" + variant.dirName
}


建立manifest任务,在manifestTask任务生成之后执行,并向android manifest文件中插入TINKER_ID,供app运行时使用.

TinkerManifestTask manifestTask = project.tasks.create("tinkerProcess${variantName}Manifest", TinkerManifestTask)
manifestTask.manifestPath = variantOutput.processManifest.manifestOutputFile
manifestTask.mustRunAfter variantOutput.processManifest

variantOutput.processResources.dependsOn manifestTask


如果开启了混淆,就会在gradle插件中构建出该任务,主要的作用是将tinker中默认的混淆信息和基准包的mapping信息加入混淆列表,这样就可以通过gradle配置自动帮开发者做一些类的混淆设置,并且可以通过applymapping的基准包的mapping文件达到在混淆上补丁包和基准包一致的目的.

boolean proguardEnable = variant.getBuildType().buildType.minifyEnabled

if (proguardEnable) {
TinkerProguardConfigTask proguardConfigTask = project.tasks.create("tinkerProcess${variantName}Proguard", TinkerProguardConfigTask)
proguardConfigTask.applicationVariant = variant
variantOutput.packageApplication.dependsOn proguardConfigTask
}


如果开启了multiDex 会在编译中根据gradle的配置和默认配置生成出要keep在main dex中的proguard信息文件,然后copy这个文件到
tinker_intermediates
下,方便开发者使用.

boolean multiDexEnabled = variant.apkVariantData.variantConfiguration.isMultiDexEnabled()

if (multiDexEnabled) {
TinkerMultidexConfigTask multidexConfigTask = project.tasks.create("tinkerProcess${variantName}MultidexKeep", TinkerMultidexConfigTask)
multidexConfigTask.applicationVariant = variant
variantOutput.packageApplication.dependsOn multidexConfigTask
}


这里把Tinker的Gradle插件流程梳理了一边,牵扯到复杂功能流程的像补丁生成的task,R文件处理task和插桩实现.这些后面会单独分析.

转载请注明出处:http://blog.csdn.net/l2show/article/details/53925543
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息