一、前言


上一篇学习了如何自定义 Gradle 插件,本篇我们来学习下 Android 对 Gradle 的扩展:Variants(变体)以及 Transform。通过扩展可以让我们在自定义 Gradle 插件时做更多的事情。

 

二、Variants(变体)


2.1 Variants 是什么

要理解 Variants 的作用,就必须先了解 buildType、flavor、dimension 与 variant 之间的关系。在 android gradle plugin V3.x 之后,每个 flavor 必须对应一个 dimension,可以理解为 flavor 的分组,然后不同 dimension 里的 flavor 会组合成一个 variant。示例代码如下所示:

    android {        ...        defaultConfig {...}        //gradle 默认就有 debug 和 release 两个 buildType        buildTypes {            debug{...}            release{...}        }        flavorDimensions "version"        productFlavors {            demo {                dimension "version"            }            full {                dimension "version"            }        }    }    

根据上述配置 Gradle 会创建以下构建变体:

  • demoDebug
  • demoRelease
  • fullDebug
  • fullRelease

在 Android 对 Gradle 插件的扩展支持之中,其中最常用的便是利用变体(Variants)来对构建过程中的各个默认的 task 进行 hook。关于 Variants 共有 三种类型,如下所示:

  • applicationVariants:只适用于 app plugin。
  • libraryVariants:只适用于 library plugin。
  • testVariants:在 app plugin 与 libarary plugin 中都适用。

2.2 Variants 的使用

我们来看看 applicationVariants 的使用,首先我们在 app.gradle 中配置 buildTypes、flavorDimensions、productFlavors 同上。然后,我们可以 使用 applicationVariants.all 在配置阶段之后去获取所有 variant 的 name 与 baseName。代码如下所示:

最后我们来执行下 gradle clean 任务:

可以看到,name 与 baseName 的区别:demoDebug 与 demo-debug 。

接下来我们来看看使用 applicationVariants.all 在配置阶段之后去修改输出的 APK 名称:

可以看到,我们上面用到了一个 releaseTime() 方法获取当前时间:

最后我们来执行以下 gradle clean:

可以看到正常修改了 apk 的名称。

最后我们来看一下如何对 applicationVariants 中的 Task 进行 Hook,我们可以在 android.applicationVariants.all 的闭包中通过 variant.task 来获取相应的 Task。代码如下所示:

然后,执行 gradle clean,其输出信息如下所示:既然可以获取到变体中的 Task,我们就可以根据不同的 Task 类型来做特殊处理。例如,我们可以利用 variants 去解决插件化开发中的痛点:编写一个对插件化项目中的各个插件自动更新的脚本,其核心代码如下所示:

至于 update_plugin 的实现,主要就是一些插件安全校验与下载的逻辑,这部分其实跟 Gradle 没有什么联系。

variant 中能获取到哪些 task 我们可以去 ApplicationVariant 的父类 BaseVariant 中去查看,比如:

2.3 Gradle 构建流程

在执行 Android 项目的构建流程,可以发现没有任何修改的情况下就已经有 30 多个Task需要执行:

 其中关键的 task 如下:

 

三、Transform


Google 官方在 Android Gradle V1.5.0 版本以后提供了 Transfrom API,允许第三方 Plugin 在打包成 .dex 文件之前的编译过程中操作 .class 文件,我们需要做的就是实现 Transform 来对 .class 文件遍历以拿到所有方法,修改完成后再对原文件进行替换即可。总的来说,Gradle Transform 的功能就是把输入的 .class 文件转换为目标字节码文件。我们可以通过 Gradle Plugin 来注册我们编写的 Transform。注册后的 Transform 会被 Gradle 包装成一个 Gradle Task,这个 TransForm Task 会在 java compile Task 执行完毕后运行。

我们来看看 Transform 的执行流程图:

3.1 Transform 的使用

下面我们来看看如何使用 Transform,首先如果是在 buildSrc 中,由于 buildSrc 的执行时机要早于任何一个 project,因此需要添加仓库:

然后,创建一个 Transform 的子类继承自 com.android.build.api.transform.Transform:

可以看到其创建步骤可以细分为五步,如下所示:

3.1.1、getName()

指定自定义 Transform 的名称。返回对应的 Task 名称。

3.1.2、getInputTypes()

可以看到这个方法返回的是一个 Set 集合,指明你自定义的这个 Transform 处理的输入类型集合。QualifiedContent.ContentType 是一个接口,它的实现类有 DefaultContentType 和 ExtendedContentType。为了方便 TransformManager 为我们封装了以下几种输入类型集合:

分别代表的是:

  • CONTENT_CLASS:表示需要处理 java 的 class 文件。
  • CONTENT_JARS:表示需要处理 java 的 class 与 资源文件。
  • CONTENT_RESOURCES:表示需要处理 java 的资源文件。
  • CONTENT_NATIVE_LIBS:表示需要处理 native 库的代码。
  • CONTENT_DEX:表示需要处理 DEX 文件。
  • CONTENT_DEX_WITH_RESOURCES:表示需要处理 DEX 与 java 的资源文件。

3.1.3、getScopes()

可以看到这个方法返回的是一个 Set 集合,用来指明自定义的 Transform 的输入文件所属的范围,这是因为 gradle 是支持多工程编译的。Scope 是一个枚举类:

可以看到目前有 5 种基本类型,分别代表的是:

  • PROJECT:只有项目内容。
  • SUB_PROJECTS:只有子项目。
  • EXTERNAL_LIBRARIES:只有外部库,
  • TESTED_CODE:由当前变体(包括依赖项)所测试的代码。
  • PROVIDED_ONLY:只提供本地或远程依赖项。

同样,为了方便,TransformManager 为我们封装了 getScope 的返回:

如果一个 Transform 不想处理任何输入,只是想查看输入的内容,那么只需在 getScopes() 返回一个空集合,然后在getReferencedScopes() 返回想要接收的范围。

public Set<? super Scope> getReferencedScopes() {     return ImmutableSet.of(); }

3.1.4、isIncremental()

isIncremental 方法用于确定是否支持增量更新,如果返回 true,TransformInput 会包含一份修改的文件列表,如果返回 false,则会进行全量编译,并且会删除上一次的输出内容。

3.1.5、transform(TransformInvocation transformInvocation)

它是 Transform 的关键方法,在 transform() 方法中,就是用来给我们进行具体的输入输出转换过程的。它是一个空实现,input 的内容将会打包成一个 TransformInvocation 对象,因为我们要想使用 input,我们需要详细了解一下 TransformInvocation 参数。

public interface TransformInvocation {    // 输入作为 TransformInput 返回    Collection getInputs();     //TransformOutputProvider 可以用来创建输出内容    TransformOutputProvider getOutputProvider();     boolean isIncremental();...}

TransformInput 可认为是所有输入文件的一个抽象,它主要包括两个部分,如下所示:

public interface TransformInput {    Collection getJarInputs();    Collection getDirectoryInputs();}public interface JarInput extends QualifiedContent {    File getFile(); //jar文件    Set getContentTypes(); // 是class还是resource    Set<? super Scope> getScopes();  //属于Scope:}

其中:

  • DirectoryInput 集合:表示以源码方式参与项目编译的所有目录结构与其目录下的源码文件。
  • JarInput 集合:表示以 jar 包方式参与项目编译的所有本地 jar 包和远程 jar 包。需要注意的是,这个 jar 所指也包括 aar。

TransformOutputProvider 表示 Transform 的输出,利用它我们可以获取输出路径等信息。

public interface TransformOutputProvider {    //根据 name、ContentType、QualifiedContent.Scope返回对应的文件( jar / directory)    File getContentLocation(String name, Set types,         Set<? super QualifiedContent.Scope> scopes, Format format);}

即我们可以通过 TransformInvocation 来获取输入,同时也获得了输出的功能。举个例子:

    @Override    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {        super.transform(transformInvocation)        println '--------------- MyCustomTransform visit start --------------- '        def startTime = System.currentTimeMillis()        def inputs = transformInvocation.inputs        def outputProvider = transformInvocation.outputProvider        // 1、删除之前的输出            if (outputProvider != null) {            outputProvider.deleteAll()        }        // Transform 的 inputs 有两种类型,一种是目录,一种是 jar包,要分开遍历            inputs.each { TransformInput input ->            // 2、遍历 directoryInputs(本地 project 编译成的多个 class⽂件存放的目录)                    input.directoryInputs.each { DirectoryInput directoryInput ->                handleDirectory(directoryInput, outputProvider)            }            // 3、遍历 jarInputs(各个依赖所编译成的 jar 文件)                    input.jarInputs.each { JarInput jarInput ->                handleJar(jarInput, outputProvider)            }        }        def cost = (System.currentTimeMillis() - startTime) / 1000        println '--------------- MyCustomTransform visit end --------------- ' p        rintln "MyCustomTransform cost : $cost s"    }

这里我们主要是做了三步处理,如下所示:

  • 删除之前的输出。
  • 遍历 directoryInputs(本地 project 编译成的多个 class ⽂件存放的目录)。
  • 遍历 jarInputs(各个依赖所编译成的 jar 文件)。

在 handleDirectory 与 handleJar 方法中则是进行了相应的 文件处理 && ASM 字节码修改。

编写完 Transform 的代码之后,我们就可以在 Plugin 的 apply 方法中加入下面代码去注册 TransformTest 的实例,代码如下所示:

 

参考链接:

配置构建变体 官方文档

浅析Android Gradle构建流程

Gradle Transform API 的基本使用

【Android】函数插桩(Gradle + ASM)

如何开发一款高性能的 gradle transform

更多相关文章

  1. android应用安全――代码安全(android代码混淆)
  2. Android开发初步之Activity与Intent
  3. android NDK扩展
  4. 如何在 Windows* 8 上为 Android* 设备安装英特尔® USB 驱动程
  5. Android中app进程ABI确定过程
  6. 关于Android(安卓)studio中httpclient不能用的问题
  7. android如何实现文件按时间先后顺序排列显示
  8. Android(安卓)开发中使用PopupWindow
  9. Android的编译过程 & Android(安卓)dex 方法限制的一些总结

随机推荐

  1. Android系统架构概况
  2. repost:Android shell 下 busybox,clear,tc
  3. 收集的android开源项目,android学习必备
  4. Android的JNI用法
  5. android 2048游戏实现
  6. Android类装载机制
  7. 如果让我重新设计一款Android App
  8. Android Studio中使用com.android.suppor
  9. Android应用程序启动Binder线程源码分析
  10. Android WebView 用法