In the previous Gradle Plugin learning notes learning record how to create in the creation of the Plugin function and how to create extensions, this printing method time-consuming to practice bytecode say pile of learning

Transform, ASM,.class bytecode file

1.1 Functions of Transform

The Transform API is provided by Android Gradle to modify the final.class bytecode file before it is compiled to dex.HenCoderThe metaphor is very appropriate and easy to understand. Think of the entire Android compilation process as a Courier, and Transform as a transit station, processing input and output to the next station. In the transform process, you do not need to care about the specific Gradle task, only care about the input processing (JAR,CLASS,RES), and the output location. Gradle builds a custom Transform first, as shown in the figure below

Photo by Calvin Lu.top /2019/12/03/…

1.2 Use of ASM

ASM is a bytecode processing framework for Java. Combined with plug-ins, it greatly reduces the difficulty of bytecode operation and simplifies the process of bytecode operation. I wouldn’t be able to do this article without ASM for a short period of time if I didn’t have a good grasp of bytecode level operations, and bytecode operations aren’t the focus of this article. A brief record of the initial use of ASM in practice is provided below.

There are other frameworks and tools for manipulating bytecode files that are not documented and practiced here. It is not appropriate to expand on the practical aspects of AOP, which I have practiced too little to avoid misguessing the results and principles

1.3. class bytecode files

For the purposes of this example, which focuses on methods, the.class bytecode file requires at least some rudimentary knowledge of the method stack, such as local variables in the method stack, and the return instruction

Take a simple example from another note for a brief explanation to create a simple method.This method is simple. It has three variables, I, a, b, takes no arguments, and has no return value

Parse the bytecode instruction structure: stack=1, operation stack volume is 1, locals=4, args_size=1, takes 1 argument, instruction 12 executes return. The method stack diagram is shown below

Understand the above tips to facilitate the following ASM use analysis

If you need to learn more about the meaning of instruction sets, refer to the official Java bytecode documentation or JVM instruction set collation


The plug-in code from the previous article has been slightly modified, and the practice of completing bytecode piling has been added to transform processing

2. Task preparation

2.1 Pile insertion target class configuration extension function

Package name, class name, method name that you want to pile

open class GrockTrackTimeExtension {
    // To provide extension modifications, variables must also be mutable and public
    var packageName = ""
    var className = ""
    var methodName = ""
}
Copy the code

Create extension functions

 val extensionFun = project.extensions
 .create("trackTime",GrockTrackTimeExtension::class.java)
Copy the code

2.2 Creating a Custom Transform

class GrockTransformSimple(val trackConfig: GrockTrackTimeExtension) : Transform() {

    override fun getName(a): String {
        return "GrockTransform"
    }

    override fun getInputTypes(a): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    override fun getScopes(a): MutableSet<in QualifiedContent.Scope> {
        return ImmutableSet.of(QualifiedContent.Scope.PROJECT)
    }

    override fun isIncremental(a): Boolean {
        return false
    }

    override fun transform(transformInvocation: TransformInvocation){}}Copy the code
  • Construct the accepting configuration extension function
  • getNameThe name of the task
  • getInputTypesThe input type accepted, specified as a CLASS bytecode file
  • getScopesSpecify scope of impact for the entire project (other types include subprojects, external dependencies, and so on)
  • isIncrementalIncremental compile time intervention
  • transformProcessing. Class, JAR, RES input and output “key”

2.3 Registering the Transform in the plug-in

Pass the extension function to the Transform and register the custom Transform for the Android build task

    override fun apply(project: Project) {
        // Create the extension function, the extension function name, bind the extension function class
        val extensionFun = project.extensions.create("trackTime",GrockTrackTimeExtension::class.java)
        val transform = GrockTransform(extensionFun)
        // Get the Android {} extension
        val baseExtension = project.extensions.getByType(BaseExtension::class.java)
        // Register the transformation in the Android extension function
        baseExtension.registerTransform(transform = transform)
    }
Copy the code

A little bit of the implementation of the registration method is to add it to a TransFrom queue and then to TaskManager to take it out and iterate over it to add TransformManager. Finally, the Task is registered by The TransformTask into the TaskFactory and associated with the entire Android compilation task to be executed

BaseExtension {...fun registerTransform(transform: Transform.vararg dependencies: Any) {
        _transforms.add(transform)
        _transformDependencies.add(listOf(dependencies))
    }
}
Copy the code

Change the bytecode file

Process logic: Get input confirm output > Filter out target class> read class> read method > method pile

3.1 Prioritize input acquisition and output location

Since transform is a transfer station, it needs to give priority to fully accept the input content and output according to the established target to ensure the normal operation of the whole link. At present, we do not know the meaning of changing the output target, and we will not try it. This article only modifiers the class files of the current project source code, but the JARS are similar, with additional steps to decompress and repackage

Get the original input for operation and output to the next station according to the original output target

    override fun transform(transformInvocation: TransformInvocation) {
        val inputs = transformInvocation.inputs
        val output = transformInvocation.outputProvider
        inputs.forEach { input ->
            // The contents of the dependent JAR package remain unchanged
            input.jarInputs.forEach { jar ->
                // Pass on to the next task
                val dest = output.getContentLocation(
                        jar.name, jar.contentTypes, jar.scopes, Format.JAR
                )
                FileUtils.copyFile(jar.file, dest)
            }
            // The source code of the current project
            input.directoryInputs.forEach { dirInput ->
            	// Process bytecode
                handlerDirInput(dirInput)
                // Pass on to the next task
                val dest = output.getContentLocation(
                        dirInput.name, dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY
                )
                FileUtils.copyDirectory(dirInput.file, dest)
            }
        }
    }
Copy the code

3.2 Using ASM to operate bytecode files

According to the configuration development function, make sure the class file target for com \ xx \ xx \ X.c lass, find all that you need from the input class class files are selected

3.2.1 Finding the target class file

    private fun handlerDirInput(dirInput: DirectoryInput) {
        val files = fileRecurse(dirInput.file.absolutePath, mutableListOf())
        val suffix = formatPackageName(trackConfig.packageName) + "\ \" + trackConfig.className + ".class"files? .forEach { classFile ->if (classFile.absolutePath.endsWith(suffix)) {
         		// Confirm the class file}}}Copy the code

FileRecurse came with Groovy, but Kotlin didn’t find one, so he had to rewrite it with the same name.

3.2.2 ASM presence: ClassVisitor and MethodVisitor

The ultimate goal is to modify the method. According to the ASM API, you need to access.class first and then access method for modification. And overwrites the modified results to the original.class file to complete the replacement.

Define a custom.class access implementation class, TrackClassVisitor, to complete the connection logic with the Transform. This class needs to take a parameter of the method name to filter the target method and hand it over to the MethodVisitor to handle

class TrackClassVisitor(val classWriter: ClassWriter, val methodName: String)
    : ClassVisitor(Opcodes.ASM5, classWriter) {
    override fun visit(version: Int, access: Int, name: String?//className: simpleName, name!
                       , signature: String? , superName:String? , interfaces:Array<out String>? {
        super.visit(version, access, name, signature, superName, interfaces)
    }
    override fun visitMethod(access: Int, name: String? , desc:String? , signature:String? , exceptions:Array<out String>?: MethodVisitor {
        if (methodName == name) {
            val methodVisitor = classWriter.visitMethod(access, name, desc, signature, exceptions)
            return TrackMethodVisitor(methodVisitor)
        }
        return super.visitMethod(access, name, desc, signature, exceptions)

    }
}
Copy the code

ASM provides classReaders and Classwriters to read and write. Class files. In this case, only simple creation and parameter passing are required to complete access modification and write overwrite

    private fun handlerDirInput(dirInput: DirectoryInput) {
        val files = fileRecurse(dirInput.file.absolutePath, mutableListOf())
        val suffix = formatPackageName(trackConfig.packageName) + "\ \" + trackConfig.className + ".class"files? .forEach { classFile ->if (classFile.absolutePath.endsWith(suffix)) {
                // Modify the bytecode file
                val classReader = ClassReader(classFile.readBytes())
                val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                // Custom class accessors
                val classVisitor = TrackClassVisitor(classWriter, trackConfig.methodName)
                / / modify
                classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                // Overwrite the source file
                val byte = classWriter.toByteArray()
                valfos = FileOutputStream(classFile.parentFile.absolutePath + File.separator + classFile.name) fos.write(byte) fos.close() }}}Copy the code

3.2.3 MethodVisitor person

class TrackMethodVisitor(val methodVisitor: MethodVisitor)
    : MethodVisitor(Opcodes.ASM4, methodVisitor), Opcodes {

    override fun visitCode(a) {
        println("fetchBooks visitCode")
        super.visitCode()
    }
    override fun visitInsn(opcode: Int) {
        println("fetchBooks visitInsn opcode=$opcode")
        super.visitInsn(opcode)
    }
    override fun visitEnd(a) {
        super.visitEnd()
        println("fetchBooks visitEnd")}}Copy the code
  • visitCodeHandle pre-logic, such as variable initialization
  • visitInsnPost-processing logic, for example, ending log printing
  • visitEndAccess to the end

It is worth mentioning that the tests found that both the pre – and post-logic need to be executed before super.xx. Corresponding logic is that the pre-logic is executed first (earlier than the original code) and the post-logic is executed last (later than the original code).

Logically, you just need to put the bytecode operation code generated by THE ASM plug-in in the appropriate place

3.2.4 Use the ASM plug-in tool to generate bytecode operation code

Install an ASM plug-in to generate bytecode operation code. The ASM plugin for Java is no longer available in Android Studio, but it is good that IT also works with Java. So in this article, we will use Java as the test instance and prepare the test class as BookShop. First, we will write the code after the pile is finished, and use the ASM plug-in to preview the bytecode operation code.

Test class code:

public class BookShop {

    public void fetchBooks(a) {
        // Pin code
        long nailTime = System.currentTimeMillis();
        io();
        // Pin code
        long trackTime = System.currentTimeMillis() - nailTime;
        System.out.println("trackTime=" + trackTime);
    }
    // Simulate the time consuming method
    private void io(a) {
        try {
            Thread.sleep(2000);
        } catch(InterruptedException e) { e.printStackTrace(); }}}Copy the code

The test class provides a fetchBooks method to simulate the elapsed time, calls the IO method to sleep for 2 seconds, and implements the elapsed statistics before and after it, and finally prints the result.

There's a little bit of a pitfall here. It's this IO method. Why do we have to do it separately? In fact, I know what the problem is, but I haven't explored how to solve it yet. I hope that when you read this, you can clarify something for me, is the solution in essence and not other AOP solutions oh

ASM Bytecode Operation code preview:

Open ASM Bytecode Viewer and select ASMified

The code is long, so I’ll show you the images

With knowledge of bytecode files, the code above is not hard to understand. It is worth mentioning that these line numbers should be ignored because the hard logic does not apply to other class files. For the purpose of printing method execution time, you only need to focus on the earliest and the last, so you can ignore line number confirmation.

3.2.5 Piling in MethodVisitor

    // Put pre-logic, such as variable declaration
    override fun visitCode(a) {
        println("fetchBooks visitCode")
        methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false)
        methodVisitor.visitVarInsn(Opcodes.LSTORE, 1)
        super.visitCode()
    }

    // Add post logic to judge execution position
    override fun visitInsn(opcode: Int) {
        println("fetchBooks visitInsn opcode=$opcode")
        if (opcode == Opcodes.RETURN || opcode == Opcodes.ATHROW) {
            methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System"."currentTimeMillis"."()J".false)
            methodVisitor.visitVarInsn(Opcodes.LLOAD, 1)
            methodVisitor.visitInsn(Opcodes.LSUB)
            methodVisitor.visitVarInsn(Opcodes.LSTORE, 3)
            methodVisitor.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System"."out"."Ljava/io/PrintStream;")
            methodVisitor.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder")
            methodVisitor.visitInsn(Opcodes.DUP)
            methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder"."<init>"."()V".false)
            methodVisitor.visitLdcInsn("trackTime=")
            methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false)
            methodVisitor.visitVarInsn(Opcodes.LLOAD, 3)
            methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(J)Ljava/lang/StringBuilder;".false)
            methodVisitor.visitLdcInsn("ms")
            methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."append"."(Ljava/lang/String;) Ljava/lang/StringBuilder;".false)
            methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder"."toString"."()Ljava/lang/String;".false)
            methodVisitor.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream"."println"."(Ljava/lang/String;) V".false)
            methodVisitor.visitInsn(Opcodes.RETURN)
        }
        methodVisitor.visitEnd()
        // Insert before super
        super.visitInsn(opcode)

    }
Copy the code

Remove the source logic for the preview stub code, leaving only the call to the IO method in this article

public class BookShop {

    public void fetchBooks(a) {
        io();
    }

    // Simulate the time consuming method
    private void io(a) {
        try {
            Thread.sleep(2000);
        } catch(InterruptedException e) { e.printStackTrace(); }}}Copy the code

3.2.6 Configuring the extension function of the plug-in and verifying the result

apply plugin:'com.grock'

trackTime{
     packageName = "com.grock.myplugin"
     className = "BookShop"
     methodName = "fetchBooks"
}
Copy the code

Log print:

 I/System.out: trackTime=2002ms
Copy the code

Finally, take a look at the target method of the spiked.class file, the fetchBooks method

    public void fetchBooks() {
        long var1 = System.currentTimeMillis();
        this.io();
        long var3 = System.currentTimeMillis() - var1;
        System.out.println("trackTime=" + var3 + "ms");
    }
Copy the code

Next project: Trying to figure out how to properly handle method stack variable writes when ASM operates on bytecode

If there are any errors, please correct them

END