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

基于Tinker V1.7.5

这篇文章主要分析一下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’文件,映射GradlepackageConfig中的配置属性, 并对外暴露访问这些map属性的方法.同时还提供了获取基准包manifest中meta的方法,但是这些方法在这个版本中并没有使用.

  • TinkerSevenZipExtension

    7zip路径配置项, 映射GradesevenZip中的属性.获取到以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

    1. 如果开启了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...'."
          )
      }
      
    2. 根据当前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
      }
      
    3. 建立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
      
    4. 如果开启了混淆,就会在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
      }
      
    5. 如果开启了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

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值