Gradle学习系列(五):Gradle Transform

概述

又开始了一个新的系列,这个系列学习Gradle,目标就是彻底理解Gradle,主要还是做下自己理解的笔记,防止忘记

Gradle系列(一):Groovy学习

Gradle学习系列(二):Gradle核心解密

Gradle学习系列(三):Gradle插件

Gradle学习系列(四):Gradle依赖

Gradle学习系列(五):Gradle Transform

简介

Google从 Android Gradle 1.5.0开始提供了Transform API,Gradle Transform是Android 官方提供给开发者在项目构建阶段(class->dex期间)用来修改.class文件的一套标准API,把class文件转换为字节码,然后操作字节码,目前比较常用的就是,字节码插桩技术

Android的打包过程

image.png

通过上图我们可以知道,我们需要在红色箭头处通过Transform API拿到应用程序的class文件,然后借助AMS之类的库,对class文件中的方法进行遍历,然后找到我们需要改动的方法,修改目标方法,插入我们的代码保存,这就是字节码插桩技术

Transform相关方法介绍

实现一个Transform需要创建一个Gradle插件,至于插件相关可以看我之前的文章Gradle学习系列(三):Gradle插件
,这里就直接从Transform开始讲了

扫描二维码关注公众号,回复: 13155249 查看本文章
class RenxhTransform extends Transform {
    
    

    @Override
    String getName() {
    
    
        return null
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
    
    
        return null
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
    
    
        return null
    }

    @Override
    boolean isIncremental() {
    
    
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    
    
        super.transform(transformInvocation)
    }
}

可以看到如果实现一个Transform主要有四个方法,下面依次讲一下这四个方法

getName

返回transform的名称,一个应用内可以有多个transform,因此需要一个名称进行标识,方便后面调用

那么最终的名字是怎么组成的呢?

在gradle plugin 的源码中有一个TransformManager的类,他的主要作用就是管理所有的Transform子类,里面一个方法getTaskNamePrefix就是名字的规则

 static String getTaskNamePrefix(@NonNull Transform transform) {
    
    
        StringBuilder sb = new StringBuilder(100);
        sb.append("transform");

        sb.append(
                transform
                        .getInputTypes()
                        .stream()
                        .map(
                                inputType ->
                                        CaseFormat.UPPER_UNDERSCORE.to(
                                                CaseFormat.UPPER_CAMEL, inputType.name()))
                        .sorted() // Keep the order stable.
                        .collect(Collectors.joining("And")));
        sb.append("With");
        StringHelper.appendCapitalized(sb, transform.getName());
        sb.append("For");

        return sb.toString();
    }

名字是以transform开头,然后拼接ContentType这个下面详细讲,ContentType之间用add拼接,然后加上with最后拼接getName返回的name

getInputTypes

获取输入类型,ContentType表示类型,我们看下源码

    enum DefaultContentType implements ContentType {
    
    
        /**
         * The content is compiled Java code. This can be in a Jar file or in a folder. If
         * in a folder, it is expected to in sub-folders matching package names.
         */
        CLASSES(0x01),

        /** The content is standard Java resources. */
        RESOURCES(0x02);

        private final int value;

        DefaultContentType(int value) {
    
    
            this.value = value;
        }

        @Override
        public int getValue() {
    
    
            return value;
        }
    }

这里表示输入类型包括俩种,CLASSES 和 RESOURCES分别代表java的class文件和资源文件

getScopes

这个是指Transform需要处理那些输入文件,官方文档一共有7中范围

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

看下源码

 enum Scope implements ScopeType {
    
    
        /** Only the project (module) content */
        PROJECT(0x01),
        /** Only the sub-projects (other modules) */
        SUB_PROJECTS(0x04),
        /** Only the external libraries */
        EXTERNAL_LIBRARIES(0x10),
        /** Code that is being tested by the current variant, including dependencies */
        TESTED_CODE(0x20),
        /** Local or remote dependencies that are provided-only */
        PROVIDED_ONLY(0x40),

        /**
         * Only the project's local dependencies (local jars)
         *
         * @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
         */
        @Deprecated
        PROJECT_LOCAL_DEPS(0x02),
        /**
         * Only the sub-projects's local dependencies (local jars).
         *
         * @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}
         */
        @Deprecated
        SUB_PROJECTS_LOCAL_DEPS(0x08);

        private final int value;

        Scope(int value) {
    
    
            this.value = value;
        }

        @Override
        public int getValue() {
    
    
            return value;
        }
    }

如果范围越小,我们需要处理文件就越少,处理的速度就会越快

isIncremental

表示是否支持增量编译,一个自定义的Transform在可能的情况下,支持增量编译,可以节省一些编译的时间和资源

transform

transform方法的参数TransformInvocation是一个接口,提供一些关于输入的基本信息,利用这些接口就可以获得编译流程中的class文件进行操作

image.png

从上图可以看到,其实整体的处理流程还是很简单的,就是从TransformInvocation获取输入,然后按照class文件夹和jar集合进行遍历,拿到所有的class文件,进行处理

  @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    
    
        super.transform(transformInvocation)
        printCopyRight()

        TransformOutputProvider transformOutputProvider = transformInvocation.getOutputProvider()
        List<TransformInput> inputs= transformInvocation.getInputs()

        transformInvocation.getInputs().each {
    
     TransformInput transformInput ->
            transformInput.jarInputs.each {
    
     JarInput jarInput ->
                println("jar=" + jarInput.name)
            }

            transformInput.directoryInputs.each {
    
     DirectoryInput directoryInput ->

                directoryInput.getFile().eachFile {
    
     File file ->
                    printFile(file)
                }

            }
        }
    }

TransformInput

TransformInput指的是输入文件的一个抽象,包括

  • DirectoryInput集合
    指源码方式参与项目编译的所有目录结构以及其中的源码文件
  • JarInput集合

是指以jar包的形式参与项目编译的所有的本地jar包和远程jar包(包括aar)

TransformOutputProvider

指的是Transform的输出,通过他可以获取输出路径信息

实战

第一步

首先我们要创建一个插件项目,至于插件相关可以看我之前的文章Gradle学习系列(三):Gradle插件

image.png

与上次相比,这次多了引入了 implementation 'com.android.tools.build:gradle:3.4.1'因为这次需要用到android plugin中的api

第二步

创建文件RenxhTransform.groovy继承Transform

package com.renxh.cusplugin

import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils
import org.gradle.api.Project


class RenxhTransform extends Transform {
    
    

    Project mProject


    RenxhTransform(Project project) {
    
    
        this.mProject = project;
    }

    @Override
    String getName() {
    
    
        return "RenxhTransform"
    }

    /**
     * 需要处理的数据类型,有两种枚举类型
     * CLASSES 代表处理的 java 的 class 文件,RESOURCES 代表要处理 java 的资源
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
    
    
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
     * 1. EXTERNAL_LIBRARIES        只有外部库
     * 2. PROJECT                   只有项目内容
     * 3. PROJECT_LOCAL_DEPS        只有项目的本地依赖(本地jar)
     * 4. PROVIDED_ONLY             只提供本地或远程依赖项
     * 5. SUB_PROJECTS              只有子项目。
     * 6. SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)。
     * 7. TESTED_CODE               由当前变量(包括依赖项)测试的代码
     * @return
     */
    @Override
    Set<? super 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)
        printCopyRight()
        TransformOutputProvider transformOutputProvider = transformInvocation.getOutputProvider()
        transformInvocation.getInputs().each {
    
     TransformInput transformInput ->
            transformInput.jarInputs.each {
    
     JarInput jarInput ->

                println("jar=" + jarInput.name)

                processJarInput(jarInput, transformOutputProvider)
            }

            transformInput.directoryInputs.each {
    
     DirectoryInput directoryInput ->

                if (directoryInput.file.isDirectory()) {
    
    
                    FileUtils.getAllFiles(directoryInput.file).each {
    
     File file ->
                        println(file.name)
                    }
                }

                processDirectoryInputs(directoryInput, transformOutputProvider)


            }
        }
    }


    static void printFile(File file) {
    
    
        if (file.isDirectory()) {
    
    
            File[] files = file.listFiles()
            files.each {
    
     File file1 ->
                if (file1.isDirectory()) {
    
    
                    printFile(file1)
                } else {
    
    
                    println("File = " + file.name)
                }
            }
        } else {
    
    
            println("File = " + file.name)
        }
    }

    static void printCopyRight() {
    
    
        println()
        println("******************************************************************************")
        println("******                                                                  ******")
        println("******                欢迎使用 RenxhTransform 编译插件                    ******")
        println("******                                                                  ******")
        println("******************************************************************************")
        println()
    }

    static void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
    
    
        File dest = outputProvider.getContentLocation(
                jarInput.getFile().getAbsolutePath(),
                jarInput.getContentTypes(),
                jarInput.getScopes(),
                Format.JAR)

        // to do some transform

        // 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
        FileUtils.copyFile(jarInput.getFile(),dest)
    }

    static void processDirectoryInputs(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
    
    
        File dest = outputProvider.getContentLocation(directoryInput.getName(),
                directoryInput.getContentTypes(), directoryInput.getScopes(),
                Format.DIRECTORY)
        // 建立文件夹
        FileUtils.mkdirs(dest)

        // to do some transform

        // 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
        FileUtils.copyDirectory(directoryInput.getFile(),dest)
    }
}

可以看到其实最主要的代码在transform()方法中,上面主要是打印了我们自定义的文本,然后分别遍历directory和jar包然后打印出他们的名字,这里没有对文件进行处理,最后把输入文件拷贝到目标目录下,需要注意的是即使我们没有对文件做任何处理,我们仍然需要把输入文件拷贝到目标目录下,否则下一个Task就没有TansformInput,如果我们将input目录复制到output指定目录,会导致最后的打包的apk缺少class

第三步

在插件中注册自定的transform

class CustomPlugin implements Plugin<Project> {
    
    

    @Override
    void apply(Project project) {
    
    

        AppExtension appExtension = project.getExtensions().findByType(AppExtension.class)
        appExtension.registerTransform(new RenxhTransform(project))
        
        }

app 的 build.gradle 里我们通常会采用插件 apply plugin: ‘com.android.application’ ,而在 library module 中则采用插件 apply plugin: ‘com.android.library’,我们看下Gradle的源码com.android.application插件对应的实现类是AppPlugin

image.png

AppPlugin中添加Extension就是AppExtension,源码如下,主要看createExtension

public class AppPlugin extends BasePlugin implements Plugin<Project> {
    
    
    @Inject
    public AppPlugin(Instantiator instantiator, ToolingModelBuilderRegistry registry) {
    
    
        super(instantiator, registry);
    }

    protected BaseExtension createExtension(Project project, ProjectOptions projectOptions, Instantiator instantiator, AndroidBuilder androidBuilder, SdkHandler sdkHandler, NamedDomainObjectContainer<BuildType> buildTypeContainer, NamedDomainObjectContainer<ProductFlavor> productFlavorContainer, NamedDomainObjectContainer<SigningConfig> signingConfigContainer, NamedDomainObjectContainer<BaseVariantOutput> buildOutputs, ExtraModelInfo extraModelInfo) {
    
    
        return (BaseExtension)project.getExtensions().create("android", AppExtension.class, new Object[]{
    
    project, projectOptions, instantiator, androidBuilder, sdkHandler, buildTypeContainer, productFlavorContainer, signingConfigContainer, buildOutputs, extraModelInfo});
    }

    public void apply(Project project) {
    
    
        super.apply(project);
    }

    //省略...
}

这里添加了androidExtension实现类是AppExtension,我们的transform最后添加到了AppExtension的基类BaseExtension的容器中了

看下效果

执行 终端执行./gradlew app:assemble 命令

image.png

已经达到想要的效果

参考

Android Gradle Transform 详解

Gradle Transform + ASM 探索

猜你喜欢

转载自blog.csdn.net/qq_34760508/article/details/115616660