Using the Android Transform API

This article was transcoded by Jianyue SimpRead, the original address blog.csdn.net

What is TransformAPI

As of 1.5.0-beta1, the Gradle plugin includes a Transform API that allows 3rd party plugins to manipulate compiled class files before converting them to dex files. (API was present in 1.4.0-beta2, but completely revised in 1.5.0-beta1)
The goal of this API is to simplify the task of injecting custom class operations without handling them, and to provide more flexibility for the content of operations. Internal code handling (jacoco, progard, multi-dex) has all been moved to this new mechanism in 1.5.0-beta1.
NOTE: This only applies to javac/dx code paths. Jack does not currently use this API.

The above is the Android official introduction to TransformAPI, the address is here

To put it simply, TransformAPI allows us to do some operations on the bytecode after compiling and packaging the Android project, after the source code is compiled into a class bytecode, and before it is processed into a dex file.

Write Transform

This blog post introduces how to use TransformAPI in an Android project by writing a TransformAPI instance. Let’s implement it with this article:

  1. Use Android Studio to create an Android project, here I named it TransformDemo

  2. Use the buildSrc method to create a gradle plug-in. If you don’t know this method, you can refer to my previous blog post: Android Gradle Plug-in Development Basics The created plug-in directory structure is as follows:

  3. Create a build.gradle file in the buildSrc directory, and add the following code:

    apply plugin: 'groovy'
    apply plugin: 'maven'
    
    repositories {<!-- -->
        google()
        mavenCentral()
    }
    
    dependencies {<!-- -->
        implementation gradleApi()
        implementation localGroovy()
        implementation 'com.android.tools.build:gradle:4.2.2'
    }
    
    sourceSets {<!-- -->
        main {<!-- -->
            java {<!-- -->
                srcDir 'src/main/java'
            }
            resources {<!-- -->
                srcDir 'src/main/resources'
            }
        }
    }
    
    java {<!-- -->
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    
    
  4. Create a MyPlugin class in the plugin package com.test.plugin, the code is as follows:

    package com.test.plugin;
    
    import com.android.build.gradle.BaseExtension;
    
    import org.gradle.api.Plugin;
    import org.gradle.api.Project;
    
    public class MyPlugin implements Plugin<Project> {<!-- -->
    
        @Override
        public void apply(Project project) {<!-- -->
        // Register Transform to the plug-in, and the Transform will be executed automatically when the plug-in is executed
            BaseExtension ext = project.getExtensions().findByType(BaseExtension.class);
            if (ext != null) {<!-- -->
                ext. registerTransform(new MyTransform());
            }
        }
    }
    
    
  5. Create the MyTransform class, the code is as follows, and the specific code explanation is placed later:

    package com.test.plugin;
    
    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.TransformInvocation;
    import com.android.build.gradle.internal.pipeline.TransformManager;
    
    import java.io.IOException;
    import java.util.Set;
    
    public class MyTransform extends Transform {<!-- -->
    
        @Override
        public String getName() {<!-- -->
            // The task name in the final execution is transformClassesWithMyTestFor[XXX] (XXX is Debug or Release)
            return "MyTest";
        }
    
        @Override
        public Set<QualifiedContent.ContentType> getInputTypes() {<!-- -->
            return TransformManager. CONTENT_CLASS;
        }
    
        @Override
        public Set<? super QualifiedContent. Scope> getScopes() {<!-- -->
            return TransformManager. SCOPE_FULL_PROJECT;
        }
    
        @Override
        public boolean isIncremental() {<!-- -->
            return false;
        }
    
        @Override
        public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {<!-- -->
            super.transform(transformInvocation);
            System.out.println("Hello MyTransform...");
        }
    }
    
    
    
  6. Remember to register the plug-in in the src/main/resources directory. The registration method will not be described in detail. There are detailed records in the previous article: Android Gradle Plug-in Development Basics

  7. Reference the created plugin in the build.gradle file under the app module by directly adding this configuration to the file header: apply plugin: 'com.test.plugin'

  8. Then we use Gradle’s sync tool to synchronize, and we can see the output in the log as follows:

    The Hello MyTransform... log in the above figure is what we wrote in the transform method of the MyTransform class to print.

The source code of the above Demo can be downloaded here: https://github.com/yubo725/android-transform-demo

Detailed TransformAPI

The above shows how to use TransformAPI in an Android project through a simple example. However, you may not know the detailed usage of Transform, and you don’t know where the specific application of TransformAPI is. Below I have compiled a detailed usage of TransformAPI :

Transform is an abstract class, and its source code has made detailed comments on Transform, translated into Chinese as follows:

Handles transformation of intermediate build artifacts.
For each transition added, a new task is created. The action of adding a transition is responsible for handling dependencies between tasks. This is done based on the conversion process. The output of a transformation can be consumed by other transformations, and these tasks are automatically chained together.
A transformation says what it applies to (content, scope) and what it generates (content).
Transforms receive input as a collection TransformInput, which consists of JarInputs and DirectoryInputs. Both provide information about the QualifiedContent.Scopes and QualifiedContent.ContentTypes associated with their specific content.
Output is handled by a TransformOutputProvider which allows the creation of new self-contained content, each associated with its own scope and content type. What TransformInput/Output handle is managed by the transformation system, their position is not configurable.
Best practice is to write as much output as the transform receives Jar/folder input. Combining all inputs into a single output prevents downstream transformations from processing limited scope.
While it is possible to distinguish different content types by their file extensions, this cannot be done for Scopes. Therefore, if a transform requests a range, but the only available output contains more ranges than requested, the build will fail.
If a conversion requests a single content type but the only available content includes more content than the requested type, the input files/folder will contain all files of all types, but the conversion should only read, process, and output the type it requested.
Additionally, transformations can indicate secondary input/output. These are not handled by upstream or downstream transformations, nor are they restricted by the types handled by transformations. They can be anything.
It is up to each transform to manage the location of these files and ensure that they are generated before the transform is called. This is done with additional parameters when registering the transformation.
These auxiliary I/Os allow the conversion to read but not process anything. This can be achieved by having getScopes() return an empty list and using getReferencedScopes() to indicate what to read.

To implement a custom Transform, you generally need to override the following methods. Here is a detailed explanation of each method:

getName()

The getName() method is used to specify the name of the custom Transform. When gradle executes the task, it will add the prefix and suffix to the name of the Transform, as shown in the figure above, the last The task name is in the format of transformClassesWithXXXForXXX.

getInputTypes()

It is used to indicate the input type of Transform, which can be used as a means of input filtering. There are many types defined in the TransformManager class:

// represents the class file compiled by javac, commonly used
public static final Set<ContentType> CONTENT_CLASS;
public static final Set<ContentType> CONTENT_JARS;
// The resources here only refer to java resources
public static final Set<ContentType> CONTENT_RESOURCES;
public static final Set<ContentType> CONTENT_NATIVE_LIBS;
public static final Set<ContentType> CONTENT_DEX;
public static final Set<ContentType> CONTENT_DEX_WITH_RESOURCES;
public static final Set<ContentType> DATA_BINDING_BASE_CLASS_LOG_ARTIFACT;

getScopes()

Used to indicate the scope of the Transform. Likewise, several scopes are defined in the TransformManager class:

// Note that different versions have different values
public static final Set<Scope> EMPTY_SCOPES = ImmutableSet.of();
public static final Set<ScopeType> PROJECT_ONLY;
public static final Set<Scope> SCOPE_FULL_PROJECT; // commonly used
public static final Set<ScopeType> SCOPE_FULL_WITH_IR_FOR_DEXING;
public static final Set<ScopeType> SCOPE_FULL_WITH_FEATURES;
public static final Set<ScopeType> SCOPE_FULL_WITH_IR_AND_FEATURES;
public static final Set<ScopeType> SCOPE_FEATURES;
public static final Set<ScopeType> SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS;
public static final Set<ScopeType> SCOPE_IR_FOR_SLICING;

The commonly used one is SCOPE_FULL_PROJECT , representing all Projects.

After determining the ContentType and Scope, the resource flow that the custom Transform needs to process is determined. For example, CONTENT_CLASS and SCOPE_FULL_PROJECT represent the resource flow composed of classes compiled from java in all projects.

isIncremental()

Indicates whether this Transform supports incremental compilation. It should be noted that even though true is returned, it will still return false when run in some cases.

transform(TransformInvocation transformInvocation)

Generally, some processing is done on the bytecode in this method.

TransformInvocation is an interface, the source code is as follows:

public interface TransformInvocation {

    /**
     * Returns the context in which the transform is run.
     * @return the context in which the transform is run.
     */
    @NonNull
    Context getContext();

    /**
     * Returns the inputs/outputs of the transform.
     * @return the inputs/outputs of the transform.
     */
    @NonNull
    Collection<TransformInput> getInputs();

    /**
     * Returns the referenced-only inputs which are not consumed by this transformation.
     * @return the referenced-only inputs.
     */
    @NonNull Collection<TransformInput> getReferencedInputs();
    /**
     * Returns the list of secondary file changes since last. Only secondary files that this
     * transform can handle incrementally will be part of this change set.
     * @return the list of changes impacting a {@link SecondaryInput}
     */
    @NonNull Collection<SecondaryInput> getSecondaryInputs();

    /**
     * Returns the output provider allowing to create content.
     * @return he output provider allowing to create content.
     */
    @Nullable
    TransformOutputProvider getOutputProvider();


    /**
     * Indicates whether the transform execution is incremental.
     * @return true for an incremental invocation, false otherwise.
     */
    boolean isIncremental();
}

We can get input and output function through TransformInvocation, for example:

public void transform(TransformInvocation invocation) {
    for (TransformInput input : invocation. getInputs()) {
        input.getJarInputs().parallelStream().forEach(jarInput -> {
        File src = jarInput. getFile();
        JarFile jarFile = new JarFile(file);
        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries. hasMoreElements()) {
            JarEntry entry = entries. nextElement();
            //deal with
        }
    }
}

In the follow-up article, I will record the related applications of Gradle plug-in + TransformAPI + bytecode instrumentation tool.

Reference

  • Basic usage of Gradle Transform API
  • The Transform API of the Android plugin for Gradle learning
  • Transform in detail