An overview of the

Recently, I was working on a requirement to dynamically insert some random code during the compilation of Android projects. I chose Gradle Transform technology. I remembered that IT had been a long time since I wrote a blog, so I recorded some basic uses of this technology.

In general, there are several techniques that can be used to dynamically insert some code logic and even generate some new Class classes during the compilation of Android projects:

  • APT(Annotation Processing Tool): compile-time Annotation Processing technology. It uses custom annotations and Annotation processors to generate code at compile-time, and compiles the generated code and source code into class files.
  • AspectJ: A compiler that extends the functionality of the target program by weaving aspects written by the developer into the target program during compilation.
  • Transform&Javassist: Transform is a way of manipulating bytecodes provided by Android Gradle. It implements code injection through a series of Transform processing before the class is compiled into the dex. Javassist makes it easy to modify.class files. For Javassist usage, see Javassist Usage.

You can also look at some of the concepts of AOP and IOC here, referring to the AOP-IOC overview. Before using Transform, you need to know how to customize Gradle plug-ins. You can choose the simplest way to implement Gradle plug-ins.

Android Gradle tools from version 1.5.0-beta1 provide the Transform API, which can operate on.class files before converting them to dex files. A custom Transform can be registered using a custom Gradle plugin. After the Transform is registered, it is wrapped as a Gradle Task, which is run after compile Task has been executed.

Dependencies are as follows:

implementation 'com. Android. Tools. Build: gradle: 4.4.1'
Copy the code

When you develop a plug-in in buildSrc, the build.gradle script reads as follows:

apply plugin: 'groovy'
apply plugin: 'maven'

repositories {
    google()
    jcenter()
    mavenCentral()
}

dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com. Android. Tools. Build: gradle: 4.4.1'
}
Copy the code

The Transform processing process is shown as follows (the picture is from the network) :

Transform

The Transform task is an abstract class that needs to be overridden to implement a custom Transform task.

class InjectTransform extends Transform {

    @Override
    String getName(a) {
        return null
    }

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

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

    @Override
    boolean isIncremental(a) {
        return false
    }

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

getName

Specifies the name of the Transform that corresponds to the name of the Task that the Transform represents. For example, if the return value is InjectTransform, Compiled can see called transformClassesWithInjectTransformForxxx task.

getInputTypes

Specifies the input type of the Transform processing. There are a number of types defined in TransformManager:

public static final Set<ScopeType> EMPTY_SCOPES = ImmutableSet.of();

// Represents a class file compiled by javac
public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES);
// The resources here refer only to Java resources
public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);
public static final Set<ContentType> CONTENT_NATIVE_LIBS = ImmutableSet.of(NATIVE_LIBS);
public static final Set<ContentType> CONTENT_DEX = ImmutableSet.of(ExtendedContentType.DEX);
public static final Set<ContentType> CONTENT_DEX_WITH_RESOURCES = ImmutableSet.of(ExtendedContentType.DEX, RESOURCES);
Copy the code

Many of these types do not allow custom transforms, and we use CONTENT_CLASS to manipulate Class files.

getScopes

Specifies the range to which the Transform input file belongs, because Gradle supports multi-project compilation. There are the following:

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),

    @Deprecated
    PROJECT_LOCAL_DEPS(0x02),
    @Deprecated
    SUB_PROJECTS_LOCAL_DEPS(0x08);

    private final int value;

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

    @Override
    public int getValue(a) {
        returnvalue; }}Copy the code

There are several scopes defined in the TransformManager class:

public static final Set<ScopeType> PROJECT_ONLY = ImmutableSet.of(Scope.PROJECT);
public static final Set<ScopeType> SCOPE_FULL_PROJECT = ImmutableSet.of(Scope.PROJECT, Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES);
public static final Set<ScopeType> SCOPE_FULL_WITH_FEATURES = new ImmutableSet.Builder<ScopeType>().addAll(SCOPE_FULL_PROJECT).add(InternalScope.FEATURES).build();
public static final Set<ScopeType> SCOPE_FEATURES = ImmutableSet.of(InternalScope.FEATURES);
public static final Set<ScopeType> SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS = ImmutableSet.of(Scope.PROJECT, InternalScope.LOCAL_DEPS);
public static final Set<ScopeType> SCOPE_FULL_PROJECT_WITH_LOCAL_JARS = new ImmutableSet.Builder<ScopeType>().addAll(SCOPE_FULL_PROJECT).add(InternalScope.LOCAL_DEPS).build();
Copy the code

The common one is SCOPE_FULL_PROJECT, which represents all projects.

Once you have identified the ContentType and Scope, you have identified the resource flow that the custom Transform needs to handle. For example, CONTENT_CLASS and SCOPE_FULL_PROJECT represent the resource flow of Java compiled classes in all projects.

isIncremental

Indicates whether the Transform supports incremental compilation. Sometimes even though it returns true, in some cases it will return as false.

transform

Transform is an empty implementation, and the contents of the input will be packaged into a TransformInvocation object.

TransformInvocation

Take a look at the interface definition:

public interface TransformInvocation {

    / / context
    @NonNull
    Context getContext(a);

    // Input/output of transform
    @NonNull
    Collection<TransformInput> getInputs(a);

     // Returns an input that is not consumed by this Transformation
    @NonNull Collection<TransformInput> getReferencedInputs(a);

    /** * 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. */
    @NonNull Collection<SecondaryInput> getSecondaryInputs(a);

    // Returns the Output provider that allows the creation of content
    @Nullable
    TransformOutputProvider getOutputProvider(a);

    boolean isIncremental(a);
}
Copy the code

TransformInput

public interface TransformInput {
    // Represents the Jar package
    @NonNull
    Collection<JarInput> getJarInputs(a);

    // represents a directory containing class files
    @NonNull
    Collection<DirectoryInput> getDirectoryInputs(a);
}
Copy the code

TransformOutputProvider

public interface TransformOutputProvider {

    void deleteAll(a) throws IOException;

    ContentType qualifiedContent.scope returns the corresponding file (jar/directory)
    @NonNull
    File getContentLocation(
            @NonNull String name,
            @NonNull Set<QualifiedContent.ContentType> types,
            @NonNull Set<? super QualifiedContent.Scope> scopes,
            @NonNull Format format);
}
Copy the code

Example: Inject code

1. Start by creating a normal Android project.

2. Customize Gradle plug-ins, using buildSrc in this example.

There are three ways to customize Gradle plug-ins.

  • Create a new buildSrc directory with the following build.gradle content:

    apply plugin: 'groovy'
    apply plugin: 'maven'
    
    repositories {
        google()
        jcenter()
        mavenCentral()
    }
    
    dependencies {
        implementation gradleApi()
        implementation localGroovy()
        implementation 'com. Android. Tools. Build: gradle: 4.4.1'
        implementation 'org. Javassist: javassist: 3.27.0 - GA'
    }
    Copy the code
  • The Transform code:

    class InjectTransform extends Transform {
    
        private Project mProject
    
        InjectTransform(Project project) {
            this.mProject = project
        }
    
        @Override
        String getName() {
            return "InjectTransform"
        }
    
        @Override
        Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS
        }
    
        @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 {
            transformInvocation.inputs.each { input ->
                // include our hand-written Class classes as well as r.class, buildconfig.class, etc
                input.directoryInputs.each { directoryInput ->
                    String path = directoryInput.file.absolutePath
                    println("[InjectTransform] Begin to inject: $path")
    
                    // Perform the injection logic
                    InjectByJavassit.inject(path, mProject)
    
                    // Get the output directory
                    def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name,
                            directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                    println("[InjectTransform] Directory output dest: $dest.absolutePath")
    
                    // Copy the input directory to the output directory
                    FileUtils.copyDirectory(directoryInput.file, dest)
                }
    
                // Jar files, such as third-party dependencies
                input.jarInputs.each { jarInput ->
                    def dest = transformInvocation.outputProvider.getContentLocation(jarInput.name,
                        jarInput.contentTypes, jarInput.scopes, Format.JAR)
                    FileUtils.copyFile(jarInput.file, dest)
                }
            }
        }
    }
    Copy the code
  • Javassit code:

    class InjectByJavassit {
        static void inject(String path, Project project) {
            try {
                File dir = new File(path)
                if (dir.isDirectory()) {
                    dir.eachFileRecurse { File file ->
                        if (file.name.endsWith('Activity.class')) {
                            doInject(project, file, path)
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace()
            }
        }
    
        private static void doInject(Project project, File clsFile, String originPath) {
            println("[Inject] DoInject: $clsFile.absolutePath")
            String cls = new File(originPath).relativePath(clsFile).replace('/'.'. ')
            cls = cls.substring(0, cls.lastIndexOf('.class'))
            println("[Inject] Cls: $cls")
    
            ClassPool pool = ClassPool.getDefault()
            // Add the current path
            pool.appendClassPath(originPath)
            / / project. Android. BootClasspath in android. Jar, otherwise can't find the android related all the classes
            pool.appendClassPath(project.android.bootClasspath[0].toString())
            // Import the android.os.Bundle because the onCreate method argument has the Bundle
            pool.importPackage('android.os.Bundle')
    
            CtClass ctClass = pool.getCtClass(cls)
            / / thawing
            if (ctClass.isFrozen()) {
                ctClass.defrost()
            }
            // Get method
            CtMethod ctMethod = ctClass.getDeclaredMethod('onCreate')
    
            String toastStr = 'android.widget.Toast.makeText(this, "I am the injected code", android.widget.Toast.LENGTH_SHORT).show(); '
    
            // End insert method
            ctMethod.insertAfter(toastStr)
            ctClass.writeFile(originPath)
    
            / / release
            ctClass.detach()
        }
    }
    Copy the code
  • Register the Transform:

    class TransformPlugin implements Plugin<Project> {
    
        @Override
        void apply(Project target) {
            target.android.registerTransform(new InjectTransform(target))
        }
    }
    Copy the code

3. Reference plug-ins.

apply plugin: com.hearing.plugin.TransformPlugin
Copy the code

After introducing a plug-in into an engineering module, you can see the logs at compile time, look at the class files, and see the inserted code.

If the content of the article is wrong, welcome to point out, common progress! Leave a “like” if you think it’s good

  • The blog link