introduce

ASM is a general-purpose Java bytecode manipulation and analysis framework. It can be used directly in binary form to modify existing classes or dynamically generate classes. ASM provides some common bytecode conversion and analysis algorithms from which you can build custom complex conversion and code analysis tools. ASM provides similar functionality to other Java bytecode frameworks, but with a focus on performance. Because it is designed and implemented as small and as fast as possible, it is ideal for use in dynamic systems (but of course it can also be used statically, for example in compilers). ASM can add, delete, modify, and query existing Java bytecode, and pay more attention to performance. For more detailed information about ASM, see its official website: ASM.ow2.io /

Gradle Transform is introduced

Gradle Transform is an Android APP built from Gradle Plugin 1.5.0-Beta1. A standard set of apis that allows third-party plug-ins to analyze and modify.class Files in the “.class Files” to “dex” process shown above. First, let’s take a look at some of the basics of Transform.

Transform class

Let’s take a look at the Transform core class, which is defined as follows:

public abstract class Transform {

    @NonNull
    public abstract String getName(a);

    @NonNull
    public abstract Set<ContentType> getInputTypes(a);

    @NonNull
    public abstract Set<? super Scope> getScopes();

    public abstract boolean isIncremental(a);
    
    public void transform(@NonNull TransformInvocation transformInvocation)
            throws TransformException, InterruptedException, IOException {}... }Copy the code

Transform is an abstract class in which four methods are abstract and subclasses must be overridden:

getName()

The Transform of the name, and only, can appear the name app/build/intermediates/transforms directory.

getInputTypes()

Define the data types to be processed by this Transform

  • CLASSES: indicates compiled Java bytecode, which can be jar packages or folders
  • RESOURCES: represents a standard Java resource to work with

getScopes()

Define the scope of this Transform

  • PROJECT: Processes only the current module
  • SUB_PROJECTS: Only child modules are handled
  • EXTERNAL_LIBRARIES: Deals only with dependencies on the current Module, such as various JARS and AAR packages
  • TESTED_CODE: Handles only test code, including test dependencies
  • PROVIDED_ONLY: Handles dependencies provided-only

isIncremental()

Defines whether the Transform supports incremental builds.

transform(TransformInvocation transformInvocation)

Execute transform, where we will process the Java bytecode, with a look at the key classes:

  • TransformInput: Input file class, which contains:
    • Collection<DirectoryInput>: source files in the folder where the compiler is compiled
    • Collection<JarInput>: files that participate in compilation as JAR packages, such as remote or local dependencies
  • TransformOutputProvider: file output class, which can obtain output path information

Gradle Transform practice

Gradle Transform: Gradle Transform: Gradle Transform: Gradle Transform

Step 1: Create an Android Project

After creating project, create a new module of Java Library type and name it Autotracker-ASM

Step 2: Change the Module type to Java-gradle-Plugin

Add build. Gradle to Java-gradle-plugin under Autotracker-ASM Module and add dependencies. Also add maven-publish so that plugin will be published to mavenLocal. As follows:

apply plugin: 'java-gradle-plugin'
apply plugin: 'maven-publish'

afterEvaluate {
    publishing {
        publications {
            official(MavenPublication) {
                groupId 'com.growingio.android'
                artifactId 'autotracker-asm'
                version '1.0.0'
                from components.java
            }
        }
    }
}

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

Step 3: CreateTransformclass

class AutotrackTransform extends Transform {
    @Override
    public String getName(a) {
        return "autotrackTransform";
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        // Only Java class files are processed
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        // Process the entire project, including dependencies
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    @Override
    public boolean isIncremental(a) {
        return false;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws IOException {
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        // Iterate over all input files
        for (TransformInput input : inputs) {
            // Walk through all folders
            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                FluentIterable<File> allFiles = FileUtils.getAllFiles(directoryInput.getFile());
                // Walk through all the files under the folder
                for (File fileInput : allFiles) {
                    // Get the destination folder for the file output
                    File outDir = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
                    outDir.mkdirs();
                    // If it is a Java class file, print the file name
                    if (fileInput.getName().endsWith(".class")) {
                        System.err.println("Transformed class file " + fileInput.getName());
                    }
                    // Copy the file to the destination folderFileUtils.copyFileToDirectory(fileInput,outDir); }}// Iterate through all jar packages
            for (JarInput jarInput : input.getJarInputs()) {
                // Get the target file of the jar package output
                File jarOut = outputProvider.getContentLocation(jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
                // Copy the jar package to the target fileFileUtils.copyFile(jarInput.getFile(), jarOut); }}}}Copy the code

Step 4: Create the Plugin class

public class AutotrackPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        AppExtension android = project.getExtensions().findByType(AppExtension.class);
        / / register the Transform
        android.registerTransform(newAutotrackTransform()); }}Copy the code

Step 5: Register the Plugin

Under the main folder directory creation resources/meta-inf/gradle – plugins, then under this folder to create file com. Growingio. Autotracker. Properties, The filename com. Growingio. Autotracker is our name of the Plugin, namely the follow-up in the build. Add the gradle file

apply plugin: 'com.growingio.autotracker'
Copy the code

The content of the properties file is:

implementation-class=com.growingio.autotracker.asm.AutotrackPlugin
Copy the code

The equals sign is followed by the full class name of the Plugin class.

Step 6: Publish the Plugin to mavenLocal

To perform a task ‘publishOfficialPublicationToMavenLocal’, will be released the Plugin to mavenLocal, final project structure as shown in the figure below.

Step 7: Add a Plugin

We now add the mavenLocal repo and dependency to build.gradle in the project root directory as follows:

buildscript {
    repositories {
        mavenLocal()
        google()
        jcenter()
    }
    dependencies {
        classpath "Com. Android. Tools. Build: gradle: 4.2.0 - alpha07"
        classpath "Com. Growingio. Android: autotracker - asm: 1.0.0"
    }
}

allprojects {
    repositories {
        mavenLocal()
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
Copy the code

Apply the plugin in the build.gradle file in the APP Module

apply plugin: 'com.growingio.autotracker'
Copy the code

Step 8: Build the APP

We click the APP run button to view the log in the Build window as follows:

> Task :app:transformClassesWithAutotrackTransformForDebug
Transformed class file MainActivity.class
Transformed class file BlankFragment.class
Transformed class file BuildConfig.class
Copy the code

We found that the Gradle Transform is in effect.

Basic KNOWLEDGE of ASM

Now that we’ve found the pointcut to modify Java bytecode, can we start to practice AOP? Don’t worry, because ASM use or have a certain threshold, here we first briefly understand the ASM framework in a few core classes.

ClassReader

A parser for an existing Java class. This class parses bytecode that conforms to the Java class file format and invokes the corresponding access method for each attribute, method, and bytecode instruction invocation.

ClassWriter

Generates class accessors for Java classes in the form of bytecode. More precisely, this class generates a bytecode that conforms to the Java class file format. It can be used alone to generate Java classes “from scratch,” or in conjunction with one or more ClassReaders to generate modified classes from one or more existing Java classes.

ClassVisitor

Accessors that access Java classes. Mainly responsible for parsing Java class annotations, various methods, various attributes, etc.

MethodVisitor

Accessors that access Java methods and are responsible for parsing and generating Java methods.

GeneratorAdapter

A subclass of MethodVisitor, which encapsulates common methods for generating Java methods easily.

AdviceAdapter

A subclass of GeneratorAdapter. Unlike GeneratorAdapter, it is an abstract class that needs to be inherited and overwritten by the user. Access methods are executed before, after, and so on.

ASM practice

Now that we know the basics, let’s practice, again based on the above project, using the example of “weaving code while the Fragment’s onResume lifecycle method is executing” as an example. Gradle Transform creates a new class file using the ASM framework. Gradle Transform creates a new class file using the ASM framework. Gradle Transform creates a new class file.

Step 1: Define the weave code

We define a class and a static method that will be executed in the Fragment onResume method in the Module of our app as follows:

public class FragmentAsmInjector {
    private static final String TAG = "FragmentAsmInjector";

    public static void afterFragmentResume(Fragment fragment) {
        Log.e(TAG, "afterFragmentResume: fragment is "+ fragment); }}Copy the code

Step 2: DefinitionClassVisitor

Define a ClassVisitor to access all Java classes that encounter Fragment subclasses to weave into the code. The following

package com.growingio.autotracker.asm;

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;

import static org.objectweb.asm.Opcodes.ACC_PUBLIC;

class FragmentAopClassVisitor extends ClassVisitor {
    private boolean mIsFragmentClass = false;
    private boolean mHasResumeMethod = false;

    public FragmentAopClassVisitor(int api, ClassVisitor cv) {
        super(api, cv);
    }

    / * * *@paramVersion JDK version of the class file. For example, 50 represents JDK version 1.7 *@paramModifiers for the Access class, such as public, final, and so on@paramName the name of the class, but will be in the form of path, such as com. Growingio. Asmdemo. BlankFragment this class, * class in the last visit method called com/growingio asmdemo/BlankFragment *@paramSignature Generic information, null * if no generic is defined@paramSuperName Name of the superclass *@paramInterfaces List of interfaces implemented by the class */
    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        // If the parent of the class is Android.app. Fragment, the class is a Fragment
        mIsFragmentClass = "android/app/Fragment".equals(superName);
        if (mIsFragmentClass) {
            System.err.println("Find fragment class, it is "+ name); }}@Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
        // If the class is a Fragment subclass and the method is onResume, code is inserted into the method
        if (mIsFragmentClass && "onResume".equals(name) && "()V".equals(desc)) {
            mHasResumeMethod = true;
            return new ResumeMethodVisitor(Opcodes.ASM6, methodVisitor, access, name, desc);
        }
        return methodVisitor;
    }

    */ is called at the end of class access
    @Override
    public void visitEnd(a) {
        // If the class is a Fragment subclass and the onResume method is not overridden, add an onResume method
        if(mIsFragmentClass && ! mHasResumeMethod) {Public void onResume()
            GeneratorAdapter generator = new GeneratorAdapter(ACC_PUBLIC, new Method("onResume"."()V"), null.null, cv);
            // Push this onto the stack. This is actually the Fragment object
            generator.loadThis();
            / / call the super onResume ()
            generator.invokeConstructor(Type.getObjectType("android/app/Fragment"), new Method("onResume"."()V"));

            // Push this onto the stack. This is actually the Fragment object
            generator.loadThis();
            / / call the static method com. Growingio. Asmdemo. FragmentAsmInjector# afterFragmentResume (fragments fragments)
            generator.invokeStatic(Type.getObjectType("com/growingio/asmdemo/FragmentAsmInjector"), new Method("afterFragmentResume"."(Landroid/app/Fragment;) V"));

            // Call return and terminate the method
            generator.returnValue();
            generator.endMethod();
        }

        super.visitEnd();
    }

    private static final class ResumeMethodVisitor extends AdviceAdapter {
        protected ResumeMethodVisitor(int api, MethodVisitor mv, int access, String name, String desc) {
            super(api, mv, access, name, desc);
        }

        /** * call */ before the method exits
        @Override
        protected void onMethodExit(int opcode) {
            // Push this onto the stack. This is actually the Fragment object
            loadThis();
            / / call the static method com. Growingio. Asmdemo. FragmentAsmInjector# afterFragmentResume (fragments fragments)
            invokeStatic(Type.getObjectType("com/growingio/asmdemo/FragmentAsmInjector"), new Method("afterFragmentResume"."(Landroid/app/Fragment;) V"));
            super.onMethodExit(opcode); }}}Copy the code

Step 3: UseClassVisitorWalk through each class file

We modified the transform method slightly and passed all the class files through the above ClassVisitor as follows

package com.growingio.autotracker.asm;

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.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 com.google.common.collect.FluentIterable;

import org.apache.commons.io.IOUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Set;

class AutotrackTransform extends Transform {
    @Override
    public String getName(a) {
        return "autotrackTransform";
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        // Only Java class files are processed
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        // Process the entire project, including dependencies
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    @Override
    public boolean isIncremental(a) {
        return false;
    }

    @Override
    public void transform(TransformInvocation transformInvocation) throws IOException {
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        // Iterate over all input files
        for (TransformInput input : inputs) {
            // Walk through all folders
            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                FluentIterable<File> allFiles = FileUtils.getAllFiles(directoryInput.getFile());
                // Walk through all the files under the folder
                for (File fileInput : allFiles) {
                    // Get the destination folder for the file output
                    File outDir = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
                    outDir.mkdirs();
                    File fileOut = new File(outDir.getAbsolutePath(), fileInput.getName());
                    // If it is a Java class file, it will be AOP processed
                    if (fileInput.getName().endsWith(".class")) {
                        if (transformClassFile(fileInput, fileOut)) {
                            System.err.println("Transformed class file " + fileInput.getName() + " successfully");
                        } else {
                            System.err.println("Failed to transform class file "+ fileInput.getName()); }}}}// Iterate through all jar packages
            for (JarInput jarInput : input.getJarInputs()) {
                // Get the target file of the jar package output
                File jarOut = outputProvider.getContentLocation(jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
                // Copy the jar package to the target fileFileUtils.copyFile(jarInput.getFile(), jarOut); }}}private boolean transformClassFile(File from, File to) {
        boolean result;
        File toParent = to.getParentFile();
        toParent.mkdirs();
        try (FileInputStream fileInputStream = new FileInputStream(from); FileOutputStream fileOutputStream = new FileOutputStream(to)) {
            result = transformClass(fileInputStream, fileOutputStream);
        } catch (Exception e) {
            e.printStackTrace();
            result = false;
        }
        return result;
    }

    private boolean transformClass(InputStream from, OutputStream to) {
        try {
            byte[] bytes = IOUtils.toByteArray(from);
            byte[] modifiedClass = visitClassBytes(bytes);
            if(modifiedClass ! =null) {
                IOUtils.write(modifiedClass, to);
                return true;
            } else{ IOUtils.write(bytes, to); }}catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    private byte[] visitClassBytes(byte[] bytes) {
        // Parse the Java bytecode
        ClassReader classReader = new ClassReader(bytes);
        // Modify Java bytecode through ClassWriter
        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
        // Access Java bytecode one by one
        FragmentAopClassVisitor fragmentAopClassVisitor = new FragmentAopClassVisitor(Opcodes.ASM6, classWriter);
        classReader.accept(fragmentAopClassVisitor, ClassReader.SKIP_FRAMES | ClassReader.EXPAND_FRAMES);
        // Returns the modified Java bytecode
        returnclassWriter.toByteArray(); }}Copy the code

Step 4: Repackage and validate the Plugin

We rerun the executive task ‘publishOfficialPublicationToMavenLocal’, will be released the Plugin to mavenLocal, and run the APP. View the log in the Build window as follows:

> Task :app:transformClassesWithAutotrackTransformForDebug Transformed class file BuildConfig.class successfully Transformed class file MainActivity.class successfully Find fragment class, it is com/growingio/asmdemo/BlankFragment Transformed class file BlankFragment.class successfully Transformed class file  FragmentAsmInjector.class successfullyCopy the code

View the Logcat window log as follows:

E/FragmentAsmInjector: afterFragmentResume: fragment is BlankFragment{8fafb1e #1 id=0x7f0800a4}
Copy the code

Found that the weave code worked.

conclusion

We found that customizing Gradle Transform and using ASM to parse Java bytecode solved the problem that AspectJ would not be able to weave in without a new method for the corresponding class. Lambda expressions can also be solved by modifying the bytecode. So what’s the downside of ASM? Using this scheme is a relatively perfect choice at present, with the only drawback being that the ASM learning curve is steep.