• buildSrc定义插件(2)


    1. Variants 变体

    要了解变体的作用,必须首先了解 flavor、dimension、variant 这三者之间的关系,AGP3.X版本以后,每一个 falvor 必须对应一个 dimension,然后不同 dimension 里的 flavor 会组合成一个 variant

    // app\build.gradle
    android{
        flavorDimensions "size","color"
        productFlavors{
            monk{
                dimension "size"
            }
            small{
                dimension "size"
            }
            blue{
                dimension "color"
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在Android对 Gradle 插件的扩展支持中,其中最常见的便是利用 variant 来对构建过程中各默认 task 进行 hook,关于 variants的类型,一共可分为3种类型

    1. applicationVariants:只适用 app plugin
    2. libraryVariants:只适用 library plugin
    3. testVariants:以上两者都适用

    这里我们以applicationVariants来举个例子

    // app\build.gradle
    android{
        productFlavors{
            flavorA{}
            flavorB{}
            flavorC{}
        }
        // 配置阶段之后获取所有variant 的 name 和 baseName
        afterEvaluate {
            applicationVariants.all {variant->
                println "name: ${variant.name}"
                println "---baseName: ${variant.baseName}"
            }
        }
    }
    /*
    这样配置后,执行gradlew clean 会报错,所以可以通过 sync方式查看输出
    > Configure project :app
    name: flavorADebug
    ---baseName: flavorA-debug
    name: flavorBDebug
    ---baseName: flavorB-debug
    name: flavorCDebug
    ---baseName: flavorC-debug
    name: flavorARelease
    ---baseName: flavorA-release
    name: flavorBRelease
    ---baseName: flavorB-release
    name: flavorCRelease
    ---baseName: flavorC-release
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    另外一个常见的场景是利用 applicationVariants 修改输出的apk名称

    // app\build.gradle
    android{
        applicationVariants.all { variant ->
            variant.outputs.each{
                outputFileName ="app-${variant.baseName}-${variant.versionName}.apk"
                println outputFileName
            }
        }
    }
    /**
    执行 gradlew clean
    > Configure project:app
    app-debug-1.0.apk
    app-release-1.0.apk
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    对 applicationVariants 中的 Task 进行 Hook

    // app\build.gradle
    afterEvaluate {
        android.applicationVariants.all {variant->
            println "checkManifest --: ${variant.checkManifest.name}"
        }
    }
    
    /*
    > Configure project :app
    checkManifest --: checkDebugManifest
    checkManifest --: checkReleaseManifest
    */
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    根据上述例子,我们可以利用 variants 去解决插件化开发中的痛点:编写一个对插件化项目中的各个插件自动更新的脚本,示例代码如下

    // app\build.gradle
    afterEvaluate {
        android.applicationVariants.all {variant->
            def checkManifest = variant.checkManifest
            checkManifest.doFirst{
                def bt = variant.buildType.name
                if(bt == 'release' || bt == 'preview'){
                    //...
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2. Transform

    大家都知道,Google 在 AGP1.5 以后提供了 transform API,允许 plugin 在打包成 .dex 文件之前的编译过程中操作 .class 文件,我们需要做的就是利用该机制修改 class文件方法替换原文件即可;即: Gradle Transform 的功能就是把输入的 .class 文件转换为目标字节码文件

    1. TransformInput:所有输入文件的表示,包括两部分
      • DirectoryInput集合:
      • JarInput集合:包含AAR
    2. TransformOutputProvider:表示输出

    实现步骤

    1. 配置 Android DSL

      plugins {
          id 'java-library'
          id 'groovy' // groovy 语言
          id 'java-gradle-plugin'
      }
      // 由于 buildSrc 执行时机是最早的,因此需要首先添加库
      repositories {
          google()
          mavenCentral()
      }
      
      sourceSets {
          main{
              groovy{
                  srcDir 'src/main/groovy'
              }
              resources {
                  srcDir 'src/main/resource'
              }
          }
      }
      dependencies {
          implementation localGroovy()
          implementation gradleApi()
          
          // Android DSL
          implementation "com.android.tools.build:gradle:7.0.2"
      	// ASM V9.1
          implementation 'org.ow2.asm:asm:9.1'
          implementation 'org.ow2.asm:asm-commons:9.1'
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
    2. 创建 Transform 的子类

      // transfomr 虽然已经过时,但过往的插件化方案还是比较成熟,可以作为借鉴
      class MyTransform extends Transform {
          /**
           * 一、重写getName:返回对于的 Task 名称
           
           * 每个 Transform 都有一个与之对应的 Transform task,这里返回的是 taskName
           * app\build\intermediates\transforms下
           */
          @Override
          String getName() {
              return "MyTransform"
          }
          
           /**
           * 二、重写 getInputTypes:确定对那些类型的结果进行转换
           
           * 确定需要对那些类型结果进行转换,比如字节码、资源文件
           * ContentType类型有6种,前2用的较为频繁
           * 1. CONTENT_CLASS:表示需要处理的.class文件
           * 2. CONTENT_JARS:表示小的 class 和资源文件
           * 3. CONTENT_RESOURCES:表示需要处理 java 的资源文件
           * 4. CONTENT_NATIVE_LIBS:表示需要处理 native 库的代码
           * 5. CONTENT_DEX:表示需要处理 DEX 文件
           * 6. CONTENT_DEX_WITH_RESOURCES:表示需要处理 DEX 与 java 的资源文件
           */
          @Override
          Set<QualifiedContent.ContentType> getInputTypes() {
              return TransformManager.CONTENT_CLASS
          }
          
          /**
           * 三、重写 getScopes:指定插件的适用范围
           
           * 制定插件的试用范围
           * Scope类型有5种
           * 1. PROJECT:只有项目内容
           * 2. SUB_PROJECTS:只有子项目
           * 3. EXTERNAL_LIBRARIES:只有外部库
           * 4. TESTED_CODE:由当前变体(包括依赖项)所测试的代码
           * 5. PROVIDED_ONLY:只提供本地或远程依赖项
           * SCOPE_FULL_PROJECT 是一个 Scope 集合,包含 Scope.PROJECT,Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES 这三项,
           * 即当前 Transform 的作用域包括当前项目、子项目以及外部的依赖库
           */
          @Override
          Set<? super QualifiedContent.Scope> getScopes() {
              // 适用范围:通常是指定整个 project,也可以指定其它范围
              return TransformManager.SCOPE_FULL_PROJECT
          }
          
          /**
           * 四、重写 isIncremental:表示是否支持增量更新
           
           * 是否支持增量更新
           * true:transformInput会返回一份修改的文件列表
           * false:全量编译,并删除上次的输出内容
           */
          @Override
          boolean isIncremental() {
              return false
          }
          
          /**
           * 五、重写 transform :进行具体的转换过程
           
           * 下面以一个标准的 Transfrom + ASM 修改字节码的模板代码举个例子
           */
          @Override
          void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
              super.transform(transformInvocation)
              println '转换开始........................'
      
              def start = System.currentTimeMillis()
              def inputs = transformInvocation.inputs
              def outputProvider = transformInvocation.outputProvider
      
              if (outputProvider != null)
                  outputProvider.deleteAll()
      
              // transform 的 inputs 有两种类型:一种是目录,一种是jar包
              inputs.each { input ->
                  // 本地 project 编译成的多个 class 文件存放的目录
                  input.directoryInputs.each { dir ->
                      handleDirectory(dir,outputProvider)
                  }
                  // 各依赖锁编译成的 jar 文件
                  input.jarInputs.each { jar ->
                      handleJar(jar, outputProvider)
                  }
              }
              def cost = (System.currentTimeMillis() - start) / 1000
              println "转换结束............................:$cost"
          }
      
          def static EXPAND_FRAMES = 0x100
      
          // transform 通用模板代码
          static void handleDirectory(DirectoryInput input, TransformOutputProvider provider){
              // 在增量模式下可以通过 provider.changedFiles 方法获取修改的文件
              // provider.changedFiles
              if (input.file.size() == 0)return
              if (input.file.isDirectory()) {
                  //遍历以某一扩展名结尾的文件
                  input.file.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { classFile ->
                      def name = classFile.name
                      if (checkClassFile(name)) {
                          println '----------- deal with "class" file <' + name + '> -----------'
                          def classReader = new ClassReader(classFile.bytes)
                          def classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                          // todo Transform + ASM 后补
                          def classVisitor = new MyCustomClassVisitor(classWriter)
                          classReader.accept(classVisitor, EXPAND_FRAMES)
                          def codeBytes = classWriter.toByteArray()
                          def fileOutputStream = new FileOutputStream(classFile.parentFile.absolutePath + File.separator + name)
                          fileOutputStream.write(codeBytes)
                          fileOutputStream.close()
                      }
                  }
              }
              // 获取 output 目录 dest:.\app\build\intermediates\transforms\hencoderTransform\
              def destFile = provider.getContentLocation(input.name,input.contentTypes,input.scopes,Format.DIRECTORY)
              // 将 input 的目录复制到 output 指定目录
              FileUtils.copyDirectory(input.file, destFile)
          }
      
          // transform 通用模板代码
          static void handleJar(JarInput input, TransformOutputProvider provider) {
              if (input.file.absolutePath.endsWith(".jar")) {
                  // 截取文件路径的 md5 值重命名输出文件,避免同名导致覆盖的情况出现
                  def jarName = input.name
                  def md5Name = DigestUtils.md5Hex(input.file.absolutePath)
                  if (jarName.endsWith(".jar")) {
                      jarName = jarName.substring(0, jarName.length() - 4)
                  }
                  def jarFile = new JarFile(input.file)
                  def enumeration = jarFile.entries()
                  def tmpFile = new File(input.file.parent+File.separator+"class_temp.jar")
                  if (tmpFile.exists()) {
                      tmpFile.delete()
                  }
                  def jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
                  while (enumeration.hasMoreElements()) {
                      def jarEntry = enumeration.nextElement()
                      def entryName = jarEntry.name
                      def zipEntry = new ZipEntry(entryName)
                      def inputStream = jarFile.getInputStream(zipEntry)
                      if (checkClassFile(entryName)) {
                          println '----------- deal with "jar" class file <' + entryName + '> -----------'
                          // 使用 ASM 对 class 文件进行操控
                          jarOutputStream.putNextEntry(zipEntry)
                          def classReader = new ClassReader(IOUtils.toByteArray(inputStream))
                          def classWriter = new ClassWriter(classReader, org.objectweb.asm.ClassWriter.COMPUTE_MAXS)
                          // todo Transform + ASM 后补
                          def cv = new MyCustomClassVisitor(classWriter)
                          classReader.accept(cv, EXPAND_FRAMES)
                          def code  = classWriter.toByteArray()
                          jarOutputStream.write(code)
                      }else {
                          jarOutputStream.putNextEntry(zipEntry)
                          jarOutputStream.write(IOUtils.toByteArray(inputStream))
                      }
                      jarOutputStream.closeEntry()
                  }
                  jarOutputStream.closeEntry()
                  jarFile.close()
      
                  // 生成输出路径 dest:./app/build/intermediates/transforms/xxxTransform/...
                  def dest = provider.getContentLocation(jarName + md5Name,input.contentTypes, input.scopes, Format.JAR)
                  // 将 input 的目录复制到 output 指定目录
                  FileUtils.copyFile(tmpFile, dest)
                  tmpFile.delete()
              }
          }
      
          // 检查 class 文件是否需要处理
          static boolean checkClassFile(String name){
              return (name.endsWith(".class") && !name.startsWith("R\$")
                      && "R.class" != name && "BuildConfig.class" != name
                      && "android/support/v4/app/FragmentActivity.class" == name)
          }
      
      }
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124
      • 125
      • 126
      • 127
      • 128
      • 129
      • 130
      • 131
      • 132
      • 133
      • 134
      • 135
      • 136
      • 137
      • 138
      • 139
      • 140
      • 141
      • 142
      • 143
      • 144
      • 145
      • 146
      • 147
      • 148
      • 149
      • 150
      • 151
      • 152
      • 153
      • 154
      • 155
      • 156
      • 157
      • 158
      • 159
      • 160
      • 161
      • 162
      • 163
      • 164
      • 165
      • 166
      • 167
      • 168
      • 169
      • 170
      • 171
      • 172
      • 173
      • 174
      • 175
      • 176
      • 177
      • 178
      • 179
      • 180
      • 181
      • 182

    以上例子说明了 Transform 的使用方法,还需要借用 ASM 的相应 API去加载相应的 .class 文件,并解析,找到满足特定条件的 .class 文件和相关方法,最后去修改相应方法实现动态插入相应的字节码

    3. 发布插件

    发布插件分为两种形式

    1. 发布插件到本地仓库

      首先引入 maven 插件,进而在 publishing 中添加上传的仓库地址的相关配置,这样 Gradle 在执行 publishing时将生成和上传 pop.xml 文件,示例代码如下

      apply plugin: 'maven-publish'
      publishing {
          repositories {
              maven {
                  url '../repo'
              }
          }
      }
      afterEvaluate {
          println '发布了。。。。。。。。。。。。。。。。。。。。'
          publishing{
              publications{
                  debug(MavenPublication){
                      from components.java
                      groupId 'com.monk.customplugin'
                      artifactId 'custom-gradle-plugin'
                      version '0.1.0'
                      description 'this is a liberary '
                  }
              }
          }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
    2. 发布插件到远程仓库,操作方式同本地发布一样,仅仅将 url 替换成远程 maven 参考的 url 即可

  • 相关阅读:
    互联网数据的重要性
    C++----类型转换
    训练集测试集验证集的区别
    Flutter App混淆加固、保护与优化原理
    golang超时控制(转)
    卢湘仪离心机仪器盛装亮相2022第十届生物发酵展12月新国际博览中心与您相约
    仅需4步,让投资人对你“一见钟情”
    PHP 安装
    波特图笔记
    从TCP到Socket,彻底理解网络编程是怎么回事
  • 原文地址:https://blog.csdn.net/qq_37776700/article/details/127608667