An overview of the

I started a new series. The goal of this series of learning Gradle is to thoroughly understand Gradle. The main goal is to make notes on your own understanding to prevent forgetting

Gradle Series (1) : Groovy learning

Gradle Learning series (2) : Gradle core decryption

Gradle Plugins

Gradle dependencies

Gradle Transform. Gradle Transform

Introduction to the

Google has provided the Transform API since Android Gradle 1.5.0, Gradle Transform is a standard API for developers to modify. Class files during the construction phase of a project (class->dex). Gradle Transform is a standard API for developers to Transform a class file into bytecode and then manipulate the bytecode

Android’s packaging process

As we can see from the figure above, we need to get the class file of the application through the Transform API at the red arrow, then use a library like AMS to traverse the methods in the class file, find the method we need to change, modify the target method, and insert our code to save. This is called bytecode piling

This section describes the Transform related methods

To implement a Transform, you need to create a Gradle plugin. For more information about the plugin, see my previous article Gradle Learning series (3) : Gradle Plugins

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)
    }
}
Copy the code

As you can see, there are four main methods for implementing a Transform, which are described in turn

getName

Return the name of the transform. There can be more than one transform in an application, so you need a name to identify it and call it later

So how does the final name come together?

The Gradle Plugin source code contains a class TransformManager that manages all Transform subclasses. The getTaskNamePrefix method is the name rule

 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();
    }
Copy the code

The name starts with transform, and then you concatenate the ContentType and I’ll talk about that in more detail, you concatenate the ContentType with add, then with with and then you concatenate the name returned by getName

getInputTypes

To get the input type, ContentType represents the type, let’s look at the source code

    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() {
            returnvalue; }}Copy the code

CLASSES and RESOURCES represent Java class files and resource files, respectively

getScopes

This refers to the input files that Transform needs to process. There are seven ranges in the official document

  • EXTERNAL_LIBRARIES: Only external libraries are available
  • PROJECT: Only PROJECT content
  • PROJECT_LOCAL_DEPS: Only local dependencies for the project (local JAR)
  • PROVIDED_ONLY: Provides only local or remote dependencies
  • SUB_PROJECTS: Only subprojects
  • SUB_PROJECTS_LOCAL_DEPS: Only local dependencies (local JARS) for subprojects
  • TESTED_CODE: The code tested by the current variable (including dependencies)

Look at the source

 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() {
            returnvalue; }}Copy the code

The smaller the range, the fewer files we need to process, and the faster the processing will be

isIncremental

Indicates whether incremental compilation is supported. A custom Transform supports incremental compilation if possible, saving some compilation time and resources

transform

The transform method parameter, TransformInvocation, is an interface that provides some basic information about the input so that the class file in the compile process can be obtained for operation

As you can see from the figure above, the overall process is very simple. It is to retrieve the input from TransformInvocation, and then iterate through the class folder and jar set, getting all the class files, and processing

  @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)
                }

            }
        }
    }
Copy the code

TransformInput

TransformInput refers to an abstraction of the input file, including

  • DirectoryInput collection

Refers to all directory structures and source files involved in the compilation of the project in source mode

  • JarInput collection

All local and remote JAR packages (including AAR) that participate in the project compilation as jar packages.

TransformOutputProvider

Refers to the output of the Transform, through which the output path information can be obtained

In actual combat

The first step

First we need to create a plug-in project. For more information about plug-ins, see my previous article Gradle Learning series (3) : Gradle Plug-ins

More compared with last time, the introduction of implementation ‘com. Android. View the build: gradle: 3.4.1 track’ because of the need to use the android API of the plugin

The second step

Create file renxhtransform. groovy inheriting 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"
    }

    /** * Specifies the data type to be processed. There are two enumerations: CLASSES for the Java class file to be processed, RESOURCES for the Java resource to be processed * @return */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    /** * refers to the Scope of the Transform to operate on. 2. PROJECT has only PROJECT contents * 3. PROJECT_LOCAL_DEPS has only local dependencies (local JARS) of the PROJECT * 4 Only local or remote dependencies * 5. SUB_PROJECTS Only subprojects. * 6. SUB_PROJECTS_LOCAL_DEPS has only local dependencies (local JARS) for the subproject. * 7. TESTED_CODE Code tested by the current variable (including dependencies) * @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("****** Welcome to RenxhTransform compilation plugin ******")
        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 the modified bytecode to dest to interfere with the bytecode during compilation
        FileUtils.copyFile(jarInput.getFile(),dest)
    }

    static void processDirectoryInputs(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        File dest = outputProvider.getContentLocation(directoryInput.getName(),
                directoryInput.getContentTypes(), directoryInput.getScopes(),
                Format.DIRECTORY)
        // Create a folder
        FileUtils.mkdirs(dest)

        // to do some transform

        // Copy the modified bytecode to dest to interfere with the bytecode during compilation
        FileUtils.copyDirectory(directoryInput.getFile(),dest)
    }
}
Copy the code

As you can see, the main code is in the transform() method, which basically prints out our custom text, then iterates through the directory and jar packages and prints out their names, no file processing is done here, and finally copies the input file to the target directory. Note that even if we don’t do anything to the file, we still need to copy the input file to the target directory, otherwise the next Task will have no TansformInput. If we don’t copy the input directory to the output specified directory, the final packaged APK will lack classes

The third step

Register the custom transform in the plug-in

class CustomPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {

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

App build.gradle we usually use the apply plugin: ‘com.android.application’, and the Apply plugin in the Library Module: ‘com.android.library’, we look at Gradle source com.android.application plug-in corresponding to the implementation class is AppPlugin class

Add Extension to AppPlugin. Add Extension to AppPlugin

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);
    }

    / / to omit...
}
Copy the code

Here we add the Android Extension implementation class AppExtension, and our transform is finally added to the container of The base class BaseExtension of AppExtension

Look at the effect

Run the./gradlew app:assemble command

The desired effect has been achieved

reference

Android Gradle Transform

Gradle Transform + ASM exploration