Android Gradle Transform使用

Android Gradle Transform使用

  • 一.概述
  • 二.使用
    • (一)引入
    • (二)API
      • 1.QualifiedContent
      • 2.TransformInput
      • 3.TransformOutputProvider
      • 4.TransformInvocation
      • 5.ContentType
      • 6.SCOPE
      • 7.TransformManager
      • 8.Transform
    • (三)使用流程
      • 1.添加至构建
      • 2.构建流程
      • 3.transform
      • 4.修改classes文件
    • (四)输出内容
  • 三.注意事项

一.概述

gradle用于构建项目,其plugin插件用于完成一特定功能,而有些时候我们希望在插件中完成对项目内容的一些更改,这就需要我们在gradle构建过程中,获取到源文件才能进行,所幸的是,gradle plugin从1.5.0版本开始,为我们提供了Transform功能,它可以以输入输出流的链式方式,供我们对源文件进行处理

自gradle plugin3.0开始,对于引入gradle plugin有一些问题,需要进行兼容,下面会有说到

二.使用

(一)引入

一般我们在根项目中的build.gradle中的buildscript中引入gradle plugin脚本,而要引入Transform功能,需要在任意module的build.gradle中的dependencies引入

compile "com.android.tools.build:gradle:2.3.3"

这里需要注意的是,在3.0后,对于gradle plugin的仓库位置发生变化,需要在buildscript和module中的repositories中使用google()仓库:

repositories {
    google()
	...
}

引入后我们就可以看到Transform等类了

(二)API

然后我们先来看看Transform功能的结构
Android Gradle Transform使用_第1张图片

1.QualifiedContent

该接口定义了一个输入内容的基本实现,有name和file,此file可能有两种形式,文件夹和jar包,于是对应的分为两个实现类,DirectoryInput和JarInput

2.TransformInput

对于每一个输入流来说,内容既可能有一组文件夹,也可能有一组jar包,TransformInput类为一个输出流的标准实现

3.TransformOutputProvider

既然有了input,对应的就要有output,output的位置不能由我们私自决定,需要通过TransformOutputProvider的getContentLocation()获取

4.TransformInvocation

将以上输入输出流信息包装为TransformInvocation对象,这也是3.0之后的一个改变,之前只是将各个参数直接传递给Transform

5.ContentType

对于输出内容,我们可以指定想要获取的输入内,ContentType中设定了几种类型:

  • CLASSES:字节码文件

  • RESOURCES:资源文件

6.SCOPE

除了ContentType,还可以指定整个Transform的作用域,SCOPE中设定了几种类型:

  • PROJECT:主项目

  • SUB_PROJECT:子项目

  • PROJECT_LOCAL_DEPS:主项目的本地依赖

  • SUB_PROJECT_LOCAL_DEPS:子项目的本地依赖

  • EXTERNAL_LIBRARIES:依赖库

在3.0后,两个LOCAL_DEPS已经废弃了,都归为EXTERNAL_LIBRARIES

7.TransformManager

3.0开始,gradle为我们提供了TransformManager类,里面的一些常量帮我们指定了一些常用的ContentType和Scope,比如:

  • SCOPE_FULL_PROJECT:包括了PROJECT、SUB_PROJECT、EXTERNAL_LIBRARIES,也是最常用的一个

  • CONTENT_CLASS:包括了CLASSES,也是最常用的一个

8.Transform

最后我们来说Transform,其需要重写的几个方法:

  • getName():为Transform定义一个名字,不过该名字最后生成的文件名,也是拼接上了flavor、buildType等等

  • getInputTypes():该方法返回的就是一组ContentType,用于限定接受的输入内容类型

  • getScopes():该方法返回的就是一组Scope,用于限定transform的作用域

  • isIncremental():该方法是指定该Transform是否使用增量构建模式

  • transform():该方法就是实际转换时候调用的方法,3.0之前将input、outputProvider等传入,3.0开始直接将一个TransformInvocation对象传入

(三)使用流程

1.添加至构建

有了Transform,需要把它加入到构建中:

project.extensions.getByType(AppExtension).registerTransform(new MyLogTransform())

AppExtension就是我们android的gradle plugin的Extension对象,即我们在build.gradle中的android{}配置模块,其中的registerTransform方法添加一个Transform对象

2.构建流程

  1. 项目中可以加入多个Transform,比如已经有ProGuardTransform、DexTransform等
    Android Gradle Transform使用_第2张图片

  2. 每个Transform的输出作为下一个Transform的输入,前一个Transform的output,会根据下一个Transform的contentTypes和scopes,将相应的内容传入到下一个Transform的inputs中
    在这里插入图片描述

  3. Transform在添加时,会将其包装为一个Task

public <T extends Transform> Optional<AndroidTask<TransformTask>> addTransform(TaskFactory taskFactory, TransformVariantScope scope, T transform, ConfigActionCallback<T> callback) {
               ...
               this.transforms.add(transform);
				//包装为Task
               AndroidTask task1 = this.taskRegistry.create(taskFactory, new ConfigAction(scope.getFullVariantName(), taskName, transform, inputStreams, referencedStreams, outputStream, this.recorder, callback));
               ...
               return Optional.ofNullable(task1);
           }
       }
}
  1. 自定义的Transform(Task)与其他tasks之间也是有依赖关系的,比如会先执行javaCompile的task,然后应用各个自定义的Transform,最后加入proGuard和Dex等的Transform
//先加入JavaCompile的Task
setJavaCompilerTask(javacTask, tasks, variantScope);
this.createPostCompilationTasks(tasks, variantScope); 
//再加入其他Transform的Task,且应用依赖
public void createPostCompilationTasks(TaskFactory tasks, VariantScope variantScope) {
        ...
        List customTransforms = extension.getTransforms();
        List customTransformsDependencies = extension.getTransformsDependencies();
        int preColdSwapTask = 0;
        for(int multiDexClassListTask = customTransforms.size(); preColdSwapTask < multiDexClassListTask; ++preColdSwapTask) {
            Transform dexOptions = (Transform)customTransforms.get(preColdSwapTask);
            List dexTransform = (List)customTransformsDependencies.get(preColdSwapTask);
            transformManager.addTransform(tasks, variantScope, dexOptions).ifPresent((t) -> {
                if(!dexTransform.isEmpty()) {
                    t.dependsOn(tasks, dexTransform);
                }
                ...
				//add MultiDex/Dex/... Transform
            });
        }
        ...
}

3.transform

了解了Transform的结构、API的定义以及Task的构建,下面就来看看,具体如何在transform方法中进行操作项目源文件吧

class MyLogTransform extends Transform {

    @Override
    String getName() {
        return 'LogTransform'
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS//只接受classes文件(包括jar包里的)
    }

    @Override
    Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT(作用域是整个项目内的,包括子项目和依赖)
    }

    @Override
    boolean isIncremental() {
        return false//不使用增量模式构建
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
		def inputs = transformInvocation.inputs
        //inputs分为两种,一种是目录文件夹,一种是jar包
        inputs.each { TransformInput input ->
            //目录文件夹是我们的源代码和生成的R文件和BuildConfig文件等
            input.directoryInputs.each { DirectoryInput directoryInput ->
                //输出地址要通过类型、名字等,由OutputProvider决定
                def dest = outputProvider.getContentLocation(directoryInput.name + directoryInput.file.absolutePath.hashCode(),
                        directoryInput.contentTypes, directoryInput.scopes,
                        Format.DIRECTORY)
                //将input的目录复制到output指定目录,因为input一定要output,否则就丢失了(transform为链式的)
                FileUtils.copyDirectory(directoryInput.file, dest)
            }
            //jar文件一般就是依赖
            input.jarInputs.each { JarInput jarInput ->
                def dest = outputProvider.getContentLocation(jarInput.name + jarInput.file.absolutePath.hashCode(),
                        jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }
        }
    }
} 

代码的注释比较详细了,这里需要解释和注意的是:

  1. 每个input都可能有两种文件类型输入,directory和jar,所以都要遍历

  2. 每个input都需要对应的output,否则就会丢失输出

  3. output时的位置,要由TransformOutputProvider通过name、contentTypes、scope和Format决定;name要注意,在同一个contentTypes、scope、Format下name要唯一,因为输入的Input的name可能重复,导致覆盖同样的文件,一般可以使用文件绝对路径的hascode作为后缀来确保唯一

  4. TransformInvocation的referencedInputs,是只用来查看的,而不是output的,其原始内容会自动原样传入到下一个Transform的input中

4.修改classes文件

我们可以在transform中拿到源字节码文件,就可以对其内容做修改了,但是手动修改字节码还是比较难的,好在有一些帮助我们用java的方式就可以修改字节码的工具,比如javassist库:

compile 'org.javassist:javassist:3.20.0-GA'

具体的使用可以参考这篇文章。

(四)输出内容

每个Transform的中间产物(输出内容),都可以在项目的build中看到,如:
Android Gradle Transform使用_第3张图片
在项目的build文件夹中的intermediates中,有一个transforms文件夹,这是所有transform构建生成的文件存储的文件夹,里面会分为各个transform,其文件夹name其实就是我们为Transform定义的name(getName()),再往里面就是针对不同的flavor、BuildType等的文件夹,最里面就是以index命名生成的文件:3.0之前这层文件夹是按照contentTypes和scope的枚举值定义的文件夹名字,升级后使用的是文件的index,比如第一个输入流的内容,输出的文件夹名就叫0,第二个就叫1以此类推

三.注意事项

在这里插入图片描述
1.Transform会被加入到所有的variant中,即会为debug、release等所有variant构件相应的task,无法根据variant进行应用;后续官方会改正这一问题

2.所有Transform的顺序无法控制,即不要依赖其执行顺序搞事情

你可能感兴趣的:(知识积累,android相关,AOP)