基于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
全局信息相关的配置项
参数 默认值 描述 oldApk null 基准apk包的路径,必须输入,否则会报错。 ignoreWarning false 如果出现以下的情况,并且ignoreWarning为false,Tinker将中断编译。因为这些情况可能会导致编译出来的patch包带来风险:
1. minSdkVersion小于14,但是dexMode
的值为"raw";
2. 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver…);
3. 定义在dex.loader用于加载补丁的类不在main dex中;
4. 定义在dex.loader用于加载补丁的类出现修改;
5. resources.arsc改变,但没有使用applyResourceMapping编译。useSign true 在运行过程中,Tinker需要验证基准apk包与补丁包的签名是否一致,Tinker是否需要为你签名。 -
buildConfig
编译相关的配置项
参数 默认值 描述 applyMapping null 可选参数;在编译新的apk时候,Tinker通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。这个只是推荐的,但 设置applyMapping会影响任何的assemble编译
。applyResourceMapping null 可选参数;在编译新的apk时候,Tinker通过旧apk的 R.txt
文件保持ResId的分配,这样不仅可以减少补丁包的大小
,同时也避免由于ResId改变导致remote view异常
。tinkerId null 在运行过程中,Tinker需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。 -
dex
dex相关的配置项
参数 默认值 描述 dexMode jar 只能是’raw’或者’jar’。
对于’raw’模式,Tinker将会保持输入dex的格式。
对于’jar’模式,Tinker会把输入dex重新压缩封装到jar。如果你的minSdkVersion小于14,你必须选择‘jar’模式,而且它更省存储空间,但是验证md5时比’raw’模式耗时()。usePreGeneratedPatchDex flase 是否提前生成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一致,即会完全忽略所有资源的修改。 largeModSize 100 对于修改的资源,如果大于largeModSize,我们将使用bsdiff算法。这可以降低补丁包的大小,但是会增加合成时的复杂度。默认大小为100kb -
packageConfig
用于生成补丁包中的’package_meta.txt’文件
参数 默认值 描述 configField TINKER_ID, NEW_TINKER_ID configField(“key”, “value”), 默认我们自动从基准安装包与新安装包的Manifest中读取tinkerId,并自动写入configField。在这里,你可以定义其他的信息,在运行时可以通过TinkerLoadResult.getPackageConfigByName得到相应的数值。但是建议直接通过修改代码来实现,例如BuildConfig。 -
sevenZip
7zip路径配置项,执行前提是useSign为true
参数 默认值 描述 zipArtifact null 例如"com.tencent.mm:SevenZip:1.1.10",将自动根据机器属性获得对应的7za运行文件,推荐使用。 path 7za 系统中的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