preface

The use of Gradle Transform + ASM to implement code pilings has become very common. This article tries to explore how to use Transform to achieve code piling more quickly and succinct, and try to achieve

  • Through annotations, all methods in any class can be implemented to calculate the time-consuming piling method
  • The implementation of a specified method in any class (mainly for third-party libraries) is implemented by configuration to calculate the time it takes to implement the method
  • All click events in the project are inserted to facilitate the burial point or determine the location of the code
  • .

What can Transform + ASM do

In a simple way, the AGP provides the Transform interface, in the application packaging process, to Java/Kotlin compiled class file for the second write operation, insert some custom logic. The logic is usually repetitive and regular, and is most likely independent of business logic.

Some SDKS for statistics on application data will insert statistical logic in the page display and exit lifecycle functions at compile time for statistics on page display data. This very transparent implementation for developers, on the one hand, has a very low access cost, and on the other hand, reduces the intrusion of the tripartite library into the display of existing projects, reducing coupling as much as possible.

There is also a common implementation of code time statistics, which uses the system.currentTimemillis () method to record the start time at the beginning of the method body and to count the start time before the method returns. Of course, this functionality was implemented back in 2013 by JakeWharton using aspectj’s solution.

Transform Basic flow

The detailed implementation of how to create a Gradle-based plug-in project and how to register it in a Plugin will not go into detail. There are many tutorials available online, starting with the Transform implementation.

You can see that there is a fairly regular pattern of things that need to be done to implement a custom Transform. Overwriting these methods by inheriting the Transform abstract class is generally sufficient. The specific function of each method can be understood from the method name.

  • GetName Specifies the name of the transform. You can have more than one transform in an application, so you need a name tag for later debugging.
  • GetInputTypes Input type, ContentType is an enumeration, what does this input type mean? Look at the definition of this enumeration.
ContentType Click Expand
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(a) {
            returnvalue; }}Copy the code

If you’ve heard of AndResGuard being used to confuse resource files, you might be starting to see some ideas here.

  • IsIncremental Whether incremental compilation is supported. For a larger project, Gradle’s existing build process is already time consuming. The only solution to this is parallelism and caching, but many of Gradle’s tasks have dependencies, so parallelism is limited to a large extent. Therefore, the cache is the only way to break through. If possible, a custom Transform supports incremental compilation, which can save some compilation time and resources. Of course, due to the limited functionality of the Transform, it must be fully compiled every time, so be sure to delete the product of the previous compilation to avoid bugs. Details on how to implement these are covered later.

  • GetScopes defines which input files this Transform will process. ScopeType is also an enumeration. Take a look at its definition.

ScopeType Click to expand
 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(a) {
            returnvalue; }}Copy the code

Predictably, the smaller this range definition is, the less input our Transform will need to handle and the faster it will execute.

  • transform(transformInvocation: TransformInvocation?) To process input content.

Again, there will be multiple transforms in a project. The Transform you define will process the output processed by the previous Transform, and the output processed by you will be processed by the next Transform. All the transform task is generally in the app/build/intermediates/transform/this directory can be seen.

The transform ()

The parameter TransformInvocation to the transform() method is an interface that provides some basic information about the input. Using this information, we can obtain the class files in the compilation process to operate on.

As you can see from the figure above, transform handles the input in a simple way. It takes the total input from TransformInvocation and iterates through the class directory and the jar file collection respectively. (The general case is discussed here. Of course, the TransformInvocation interface also provides getReferencedInputs and getSecondaryInputs for the user to handle some special inputs, which are not shown in the figure above, so we will not discuss them for now.)

The core difficulties of Transform are as follows:

  • Correct and efficient file directory, JAR file decompression, class file IO stream processing, ensure that in this process do not lose files and wrong write
  • Efficiently find the nodes to be inserted and filter out invalid classes
  • Support for incremental compilation

practice

With some of the processes and concepts mentioned above, here’s an example (from Koala) of an annotated implementation that inserts statistical method time, parameters, and output using ASM during the transform task execution. Many thanks Koala and lijiankun24 for open source.

The effect

For the sake of later narration, let’s first look at how it was used and the end result.

Add annotations

We annotate some of the methods in MainActivity

Click to expand
class MainActivity : AppCompatActivity() {

    @Cat
    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        test()
        test2("a".100)
        test3()
        test4()
        val result = Util.dp2Px(10)}@Cat
    private fun test(a) {
        println("just test")}@Cat
    private fun test2(para1: String, para2: Int): Int {
        return 0
    }

    @Cat
    private fun test3(a): View {
        return TextView(this)}private fun test4(a){
        println("nothing")}}Copy the code

All MainActivity methods except test4() are annotated with @cat, and all methods are called.

The output log

Click to expand
The 2020-01-04 11:32:13. 784 E: ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- - the 2020-01-04 11:32:13. 784 E: │ class 's name: com/engineer/android/myapplication/MainActivity 11:32:13 2020-01-04. 784 E: │ method' s name: Test 2020-01-04 11:32:13.785 E: │ method's result: [] 2020-01-04 11:32:13.785 E: │ method's result: Null 2020-01-04 11:32:13.791 E: │ method cost time: 1ms 2020-01-04 11:32:13.791 E: └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- - the 2020-01-04 11:32:13. 791 E: ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- - the 2020-01-04 11:32:13. 791 E: │ class 's name: com/engineer/android/myapplication/MainActivity 11:32:13 2020-01-04. 792 E: │ method' s name: Test2 2020-01-04 11:32:13.792 E: │ method's arguments: [a, 100] 2020-01-04 11:32:13.792 E: │ method's result: 020-01-04 11:32:13.793 E: │ method cost time: 0ms 2020-01-04 11:32:13.793 E: └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- - the 2020-01-04 11:32:13. 794 E: ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- - the 2020-01-04 11:32:13. 795 E: │ class 's name: com/engineer/android/myapplication/MainActivity 11:32:13 2020-01-04. 795 E: │ method' s name: Test3 2020-01-04 11:32:13.796 E: │ method's result: [] 2020-01-04 11:32:13.796 E: │ method's result: android.widget.TextView{8a9397d V.ED..... . ID 0,0-0,0} 2020-01-04 11:32:13.796 E: │ method cost time: 1ms 2020-01-04 11:32:13.796 E: └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- - the 2020-01-04 11:32:13. 797 E: ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- - the 2020-01-04 11:32:13. 797 E: │ class 's name: com/engineer/android/myapplication/MainActivity 11:32:13 2020-01-04. 797 E: │ method' s name: OnCreate 2020-01-04 11:32:13.798 E: │ method's arguments: [null] 2020-01-04 11:32:13.798 E: │ method's result: Null 2020-01-04 11:32:13.798 E: │ method cost time: 156ms 2020-01-04 11:32:13.798 E: └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- -Copy the code

As you can see, the log outputs the method time, method parameters, method names, method return values, and so on for all methods except the test4() method. Let’s look at the implementation details.

Implementation details

Take the initiative to call

First of all, for one of the above functions, it should be very simple if we write the code directly.

The printed log has some method information, so you need a class to hold this information.

  • MethodInfo
data class MethodInfo(
    var className: String = "".var methodName: String = "".var result: Any? = "".var time: Long = 0.varparams: ArrayList<Any? > = ArrayList() )Copy the code

According to the general idea, we need to record the start time when the method starts, record the time again before the method return, and then calculate the time.

  • MethodManager
object MethodManager {

    private val methodWareHouse = ArrayList<MethodInfo>(1024)

    @JvmStatic
    fun start(a): Int {
        methodWareHouse.add(MethodInfo())
        return methodWareHouse.size - 1
    }

    @JvmStatic
    fun end(result: Any? , className:String, methodName: String, startTime: Long, id: Int) {
        val method = methodWareHouse[id]
        method.className = className
        method.methodName = methodName
        method.result = result
        method.time = System.currentTimeMillis() - startTime
        BeautyLog.printMethodInfo(method)
    }

}
Copy the code

There are two methods defined, start and end, which are called, as the name implies, at the beginning and end of the method, passing some key information through the arguments, and finally printing the information.

So we can call these methods from any method

    fun foo(a){
        val index =MethodManager.start()
        val start = System.currentTimeMillis()
        
        // some thing foo do
        
        MethodManager.end("".this.localClassName,"foo",start,index)
    }
Copy the code

Sure, this code is simple to write, but on the one hand, it’s unrelated to what foo is supposed to do, and it’s a little ugly to add in just to test the method once. Another thing is that if you have multiple methods that need to be checked, you have to write this code many times. As a result, there is a need to implement code piling through Transform + ASM.

Insert pile implementation

Within a method, it’s easy to write the above code ourselves, open the IDE and find the corresponding class file, locate the method to calculate the time, and insert the code before the method body starts and ends. However, for the compiler, these irregular things are very troublesome. Therefore, for convenience, we define annotations so that the compiler can quickly locate the position to be inserted during code compilation.

There’s a Cat annotation defined here, and why is it called Cat? Because cats are cute.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Cat {
}
Copy the code

According to the transform(transformInvocation: transformInvocation? Processing the flow of the input flow, we can process all the class files.

Here’s an example of processing directoryInputs

input.directoryInputs.forEach { directoryInput ->
                if (directoryInput.file.isDirectory) {
                    FileUtils.getAllFiles(directoryInput.file).forEach {
                        val file = it
                        val name = file.name
                        println("directory")
                        println("name ==$name")
                        if (name.endsWith(".class") && name ! = ("R.class")
                            && !name.startsWith("R\$") && name ! = ("BuildConfig.class")) {val reader = ClassReader(file.readBytes())
                            val writer = ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
                            val visitor = CatClassVisitor(writer)
                            reader.accept(visitor, ClassReader.EXPAND_FRAMES)

                            val code = writer.toByteArray()
                            val classPath = file.parentFile.absolutePath + File.separator + name
                            val fos = FileOutputStream(classPath)
                            fos.write(code)
                            fos.close()
                        }
                    }
                }

                valdest = transformInvocation.outputProvider? .getContentLocation( directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY ) FileUtils.copyDirectoryToDirectory(directoryInput.file, dest) }Copy the code

The operation here is simply to walk through all the class files, process all the qualified classes through the interface provided by ASM, and provide a custom ClassVisitor through the visitor pattern. In this case our custom ClassVisitor is the CatClassVisitor, using the visitor’s pattern again in the visitMethod implementation inside the CatClassVisitor to return a custom CatMethodVisitor, Internally, we determine whether the current method needs to be staked, based on the method annotations.

  override fun visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor {
        // The current method's annotation is the annotation we defined.
        if (Constants.method_annotation == desc) {
            isInjected = true
        }
        return super.visitAnnotation(desc, visible)
    }

  override fun onMethodEnter(a) {
        if (isInjected) {
            
            methodId = newLocal(Type.INT_TYPE)
            mv.visitMethodInsn(
                Opcodes.INVOKESTATIC,
                Constants.method_manager,
                "start"."()I".false) mv.visitIntInsn(Opcodes.ISTORE, methodId) ... more details ... }}override fun onMethodExit(opcode: Int) {
        if (isInjected) {

            ... other details ...

            mv.visitMethodInsn(
                Opcodes.INVOKESTATIC,
                Constants.method_manager,
                "end"."(Ljava/lang/Object; Ljava/lang/String; Ljava/lang/String; JI)V".false)}}Copy the code

As you can see, this determines where to do code stubs. The details of ASM code stubs are covered in when Java bytecode encounters ASM and will not be expanded here. You can see the source code for the implementation here.

Of course, we also need to handle the scenario where the input is jarInputs. In componentized development, we often rely on business components or basic components provided by other partners by relying on AAR packages. Or when we rely on third-party libraries, we are also relying on aar. In this case, if the processing of jarInputs is missing, the pile insertion function will be lost. However, as you can see from the above flowchart, the processing of jarInputs is only to learn more about the compression process, followed by the traversal and write operation to the class file.

Incremental compilation

One of the things that we have to talk about when we talk about Transform is incremental compilation. In fact, the implementation of incremental compilation is quite simple by looking at the several transforms that come with AGP.

if (transformInvocation.isIncremental) {
                    when(jarInput.status ? : Status.NOTCHANGED) { Status.NOTCHANGED -> { } Status.ADDED, Status.CHANGED -> transformJar( function, inputJar, outputJar ) Status.REMOVED -> FileUtils.delete(outputJar) } }else {
                    transformJar(function, inputJar, outputJar)
                }
Copy the code

All of the inputs are states, and it’s ok to do different things according to those states. Of course, incremental compilation can also be supported based on the input provided by getSecondaryInputs mentioned earlier.

Simplify the Transform process

Reviewing the transform process and three key points mentioned above, we can abstract a more general transform base class by referring to the official CustomClassTransform.

By default, it supports incremental compilation and handles file IO operations


abstract class BaseTransform : Transform() {

    // Create a BiConsumer for a class file. // Create a BiConsumer for a class file
    abstract fun provideFunction(a): BiConsumer<InputStream, OutputStream>?

    // The default class filter handles everything at the end of.class.
    open fun classFilter(className: String): Boolean {
        return className.endsWith(SdkConstants.DOT_CLASS)
    }

    // Transform Enables the switch
    open fun isEnabled(a) = true.else function ...

    // Incremental compilation is supported by default
    override fun isIncremental(a): Boolean {
        return true
    }
   

    override fun transform(transformInvocation: TransformInvocation?). {
        super.transform(transformInvocation)

        val function = provideFunction()

        ......

        if (transformInvocation.isIncremental.not()) {
            outputProvider.deleteAll()
        }

        for (ti in transformInvocation.inputs) {
            for (jarInput in ti.jarInputs) {
                 ......
                if (transformInvocation.isIncremental) {
                    when(jarInput.status ? : Status.NOTCHANGED) { Status.NOTCHANGED -> { } Status.ADDED, Status.CHANGED -> transformJar( function, inputJar, outputJar ) Status.REMOVED -> FileUtils.delete(outputJar) } }else {
                    transformJar(function, inputJar, outputJar)
                }
            }
            for (di in ti.directoryInputs) {

                ......
                
                if (transformInvocation.isIncremental) {
                    for ((inputFile, value) in di.changedFiles) {

                        ......

                        transformFile(function, inputFile, out)... }}else {
                    for (`in` in FileUtils.getAllFiles(inputDir)) {
                        if (classFilter(`in`.name)) {
                            val out =
                                toOutputFile(outputDir, inputDir, `in`)
                            transformFile(function, `in`, out)}}}}}}@Throws(IOException::class)
    open fun transformJar(
        function: BiConsumer<InputStream, OutputStream>? , inputJar:File,
        outputJar: File
    ) {
        Files.createParentDirs(outputJar)
        FileInputStream(inputJar).use { fis ->
            ZipInputStream(fis).use { zis ->
                FileOutputStream(outputJar).use { fos ->
                    ZipOutputStream(fos).use { zos ->
                        var entry = zis.nextEntry
                        while(entry ! =null && isValidZipEntryName(entry)) {
                            if(! entry.isDirectory && classFilter(entry.name)) { zos.putNextEntry(ZipEntry(entry.name)) apply(function, zis, zos) }else { // Do not copy resources
                            }
                            entry = zis.nextEntry
                        }
                    }
                }
            }
        }
    }

    @Throws(IOException::class)
    open fun transformFile(
        function: BiConsumer<InputStream, OutputStream>? , inputFile:File,
        outputFile: File
    ) {
        Files.createParentDirs(outputFile)
        FileInputStream(inputFile).use { fis ->
            FileOutputStream(outputFile).use { fos -> apply(function, fis, fos) }
        }
    }


    @Throws(IOException::class)
    open fun apply(
        function: BiConsumer<InputStream, OutputStream>? , `in` :InputStream.out: OutputStream
    ) {
        try{ function? .accept(`in`, out)}catch (e: UncheckedIOException) {
            throwe.cause!! }}}Copy the code

The details of file IO and incremental compilation are encapsulated in the transform processing process above. To unify the class write operation and secondary copy, InputStream and OutoutStream object processing.

Use annotations to implement the piling of all the methods in the class

Earlier we detailed a method’s time-consuming piling by defining the annotated Cat. However, the use of this annotation is limited to methods, and if we want to check the time of multiple methods in a class at the same time, it will be tedious. Therefore, we can simply upgrade this annotation and implement a pile-in implementation that supports time detection for all methods within the Class.

Annotations define Tiger

As the name suggests, the implementation here is a copy of the Tiger.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Tiger {
}
Copy the code

The Transform to realize

class TigerTransform : BaseTransform() {

    override fun provideFunction(a): BiConsumer<InputStream, OutputStream>? {
        return BiConsumer { t, u ->
            val reader = ClassReader(t)
            val writer = ClassWriter(reader, ClassWriter.COMPUTE_MAXS)
            val visitor = TigerClassVisitor(writer)
            reader.accept(visitor, ClassReader.EXPAND_FRAMES)
            val code = writer.toByteArray()
            u.write(code)
        }
    }

    override fun getName(a): String {
        return "tiger"}}Copy the code

By directly inherits the Transform abstract Class we just defined, we can focus on how to handle writing and input to Class files, in this case InputStream and OutputStream. Interact directly with ASM’s ClassReader and ClassWriter interfaces. You don’t have to worry about the internal details of incremental compilation, TransformInvocation output, and input IO.

Let’s look at the TigerClassVisitor

class TigerClassVisitor(classVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM6, classVisitor) {

    private var needHook = false
    private lateinit var mClassName: String

    override fun visit(
        version: Int, access: Int, name: String,
        signature: String? , superName:String? , interfaces:Array<String>? {
        super.visit(version, access, name, signature, superName, interfaces)
        println("hand class $name")
        mClassName = name
    }

    override fun visitAnnotation(desc: String? , visible:Boolean): AnnotationVisitor {
        if (desc.equals(Constants.class_annotation)) {
            println("find $desc ,start hook ")
            needHook = true
        }
        return super.visitAnnotation(desc, visible)
    }

    override fun visitMethod(
        access: Int,
        name: String? , desc:String? , signature:String? , exceptions:Array<out String>?: MethodVisitor {


        var methodVisitor = super.visitMethod(access, name, desc, signature, exceptions)

        if (needHook) {
            .... hook visitor ...
        }

        return methodVisitor
    }
}
Copy the code

The key here is the visitAnnotation method, which is the callback method where we get the annotation for the current Class, and when the annotation for that Class is equal to the Tiger annotation that we defined, then we can do the time detection code stub for all the methods in that Class, The piling of code in the visitMethod method has been implemented above.

We can go to the application of the build directory see if inserted pile code, such as the app/build/intermediates/transforms/tiger / {flavor} / {packageName} / XXX/find compile product directory.

Click on
@Tiger
public class Util {
    private static final float DENSITY;

    public Util(a) {
        int var1 = MethodManager.start();
        long var2 = System.nanoTime();
        MethodManager.end((Object)null."com/engineer/android/myapplication/Util"."<init>", var2, var1);
    }

    public static int dp2Px(int dp) {
        int var1 = MethodManager.start();
        MethodManager.addParams(new Integer(dp), var1);
        long var2 = System.nanoTime();
        int var10000 = Math.round((float)dp * DENSITY);
        MethodManager.end(new Integer(var10000), "com/engineer/android/myapplication/Util"."dp2Px", var2, var1);
        return var10000;
    }

    public static void sleep(long seconds) {
        int var2 = MethodManager.start();
        MethodManager.addParams(new Long(seconds), var2);
        long var3 = System.nanoTime();

        try {
            Thread.sleep(seconds);
        } catch (InterruptedException var6) {
            var6.printStackTrace();
        }

        MethodManager.end((Object)null."com/engineer/android/myapplication/Util"."sleep", var3, var2);
    }

    public static void nothing(a) {
        int var0 = MethodManager.start();
        long var1 = System.nanoTime();
        System.out.println("do nothing,just test");
        MethodManager.end((Object)null."com/engineer/android/myapplication/Util"."nothing", var1, var0);
    }

    static {
        int var0 = MethodManager.start();
        long var1 = System.nanoTime();
        DENSITY = Resources.getSystem().getDisplayMetrics().density;
        MethodManager.end((Object)null."com/engineer/android/myapplication/Util"."<clinit>", var1, var0); }}Copy the code

You can see that the Tiger annotated Util class already has stub code inside all of its methods. Later, when these methods are called, you can see that the method takes time. If there are methods in other classes that need the same functionality, it’s a simple thing to do but just use the Tiger annotations.

Configures a pile for a method in any class

The above implementation is based on our existing code, but sometimes when we do performance optimization, we need to calculate the time of some methods we use the open source library, for public methods may be fine, but for private methods or some other scenarios, it will be more troublesome. Need to use proxy mode (dynamic proxy or static proxy) to achieve the function we need, or other means, but this means no generality, this change library to use, may have to write a similar function again, or you can also put the three party library source code down directly to change is also possible.

Here you can actually use ASM to do a little help to simplify the work. Glide is an example here.

Glide.with(this).load(url).into(imageView);
Copy the code

Suppose (just suppose) that you now need to calculate the time of the load and into methods. How do you do that?

Consider the two implementations above, where we defined class and method names based on annotations to implement the logic of inserting statistical method time into a particular class or method. Now that the source code for these methods is not accessible, annotations can not be added, what to do? Let’s start from the root of the problem and let the user tell transform exactly which method of which class to calculate the method time.

We can define a node like this, just like the Android closure for Build. gradle.

open class TransformExtension {
    // The class is the key and the method name is worth a map
    vartigerClassList = HashMap<String, ArrayList<String? > > ()}Copy the code

Configure the information in the build.gradle file

    transform {
        tigerClassList = ["com/bumptech/glide/RequestManager": ["load"]."com/bumptech/glide/RequestBuilder": ["into"]]}Copy the code

Then in ClassVisitor and MethodVisitor, respectively, the node to be plugged is identified based on the class name and method name.

init { .... classList = transform? .tigerClassList }override fun visit(
        version: Int, access: Int, name: String,
        signature: String? , superName:String? , interfaces:Array<String>? {
        super.visit(version, access, name, signature, superName, interfaces)
        mClassName = name
        if(classList? .contains(name) ==true) { methodList = classList? .get(name) ? : ArrayList() needHook =true}}Copy the code

You can look at the results very briefly

Click on
19407-19407 E/0Cat: ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- - 19407-19407 - E / 1 cat: │ class 's name: Com/bumptech/glide/RequestManager 19407-19407 - E / 2 cat: │ method 's name: load 19407-19407 - E / 3 cat: │ method' s arguments: [http://t8.baidu.com/it/u=1484500186, & app = 86 & 1503043093 & FM = 79 f = JPEG? W = 1280 & h = 853] 19407-19407 - E/cat: │ method 's result: com. Bumptech. Glide. RequestBuilder @ 9 a29abdf 19407-19407 - E/cat: │ method cost time: Ms 1.52 in 19407-19407 E / 6 cat: └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- - 19407-19407 - E / 0 cat: ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- - 19407-19407 - E / 1 cat: │ class 's name: Com/bumptech/glide/RequestBuilder 19407-19407 - E / 2 cat: │ method 's name: into 19407-19407 - E / 3 cat: │ method' s arguments: [Target for: Androidx. Appcompat. Widget. AppCompatImageView {24 ec8f4 V.E D...... i. 0, 0, 0, 0 # 7 f08007c app: id/image}, null, com.bumptech.glide.RequestBuilder@31a00c76, com.bumptech.glide.util.Executors$1@1098060] 19407-19407 E/4Cat: │ method 's result: Target for: androidx. Appcompat. Widget. AppCompatImageView {24 ec8f4 V.E D... . I. 0,0-0,0 #7f08007c app:id/image} 19407-19407e /5Cat: │ method cost time: 5.78ms 19407-19407e /6Cat: └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- - 19407-19407 - E / 0 cat: ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- - 19407-19407 - E / 1 cat: │ class 's name: Com/bumptech/glide/RequestBuilder 19407-19407 - E / 2 cat: │ method 's name: into 19407-19407 - E / 3 cat: │ method' s arguments: [androidx appcompat. Widget. AppCompatImageView {24 ec8f4 V.E D...... i. 0, 0, 0, 0 # 7 f08007c app: id/image}]. 19407-19407 E/cat: │ method 's result: Target for: androidx. Appcompat. Widget. AppCompatImageView {24 ec8f4 V.E D... . I. 0,0-0,0 #7f08007c app:id/image} 19407-19407e /5Cat: │ method cost time: 10.88ms 19407-19407e /6Cat: └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- -Copy the code

The load method of the RequestManager has printed the complete method information. In this case, we can check which urls have been loaded using Glide. Of course, as you can see from the log above, the RequestBuilder prints two into methods. It is a bit rough to pile in methods by name. If there are multiple methods with the same name in the target class (method overload), then these methods will be piled. This problem can also be more accurately matched by providing method desc (that is, method parameters, return value, etc.). However, if this is just a test, so coarser-grained can also be, after all, such a three-party library pile is more hack, online environment is best not to use.

Statistics of click events in Android

Click events here generally refer to click events that implement the View.OnClickListener interface

In a mature App, there will be buried points, which are actually statistics of user behavior. What page was opened, what button was clicked, what function was used? Through the statistics of these behaviors, the most commonly used functions of users can be learned through the data to facilitate product decision-making.

The thing about clicking is, first of all, how do we do it? Either you implement the view. OnClickListener interface and expand it in the onClick method. Or an anonymous inner class, also expanded in the onClick method. Therefore, how to determine the onClick method becomes a matter of concern. We can’t follow the simple rule of equals before. Because this cannot avoid the problem of method name or parameter name, suppose that a friend writes a common method with the same name as onClick(View View), the actual hook node we locate may not be the node where the click event occurs. Therefore, we need to make sure the ASM access class implements the android. View. The view. An OnClickListener this interface.

    override fun visit(
        version: Int,
        access: Int,
        name: String? , signature:String? , superName:String? , interfaces:Array<out String>? {
        super.visit(version, access, name, signature, superName, interfaces) className = name interfaces? .forEach {if (it == "android/view/View\$OnClickListener") {
                    println("bingo , find click in class : $className")
                    hack = true}}}Copy the code

The implementation is simple, too. The visit method of the ClassVisitor provides all the interfaces implemented by the current class, so a quick judgment is in order. Of course, we can also determine other interfaces at the same time. For example, we want to pile all the selected events of the TabBar in the application. Can judge whether the current class implements the com. Google. Android. Material. Bottomnavigation. BottomNavigationView. OnNavigationItemSelectedListener this interface.

Since Transform accesses all classes generated by javac compilation, including anonymous inner classes, you can handle both ordinary and anonymous inner classes uniformly here. (See this article for details on how anonymous inner classes and ordinary classes use ASM.)

After determining whether the current class implements the interface, the next step is to make a more precise match between the method name and the method parameters and the return value.

override fun visitMethod(
        access: Int,
        name: String? , desc:String? , signature:String? , exceptions:Array<out String>?: MethodVisitor {
        var methodVisitor = super.visitMethod(access, name, desc, signature, exceptions)
        if (hack) {
            if (name.equals("onClick") && desc.equals("(Landroid/view/View;) V")) {
                methodVisitor =
                    TrackMethodVisitor(className, Opcodes.ASM6, methodVisitor, access, name, desc)
            }
        }

        return methodVisitor
    }
Copy the code

The actual code for the pile-in is essentially similar to the previous implementations, so I won’t post it here. Here’s a quick look at the effect.

E / 0 track: ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- - E / 1 track: │ class 's name: Com. Engineer. Android. Myapplication. SecondActivity E / 2 track: │ view 's id: Com. Engineer. Android. Myapplication: id/button E / 3 track: │ view 's package name: com.engineer.android.myapplication E/4Track: └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- - -- - ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ -- -- -- -- -- -Copy the code

When a click event occurs, we can get the class that implements the click event, the id of the click event, and the package name. From this information, we can roughly know which page (which business) the click event is on. Therefore, this implementation can also help us locate the code. Sometimes it is difficult to locate the corner of the code where the function you are using is in the face of completely unfamiliar code. With the Transform implementation, you can simply locate the scope.

In my own testing, I found that using lambda expressions to implement anonymous inner classes in Java would not be treated as normal anonymous inner classes and would not generate additional anonymous classes. Therefore, for click events implemented using lambda expressions, this cannot be handled. (I haven’t thought of any good alternatives yet)

See here, do you have some ideas? Is it possible to implement code inserts based on the Activity/Fragment lifecycle to count the time and number of times the page is displayed? Is it possible to consider removing log.d from the code? Is it possible to remove code that does not have references and calls? (This may be a little difficult)

conclusion

To be clear, all of the above implementations are explorations of the Transform + ASM technology stack, just a simple study and understanding of what Transform + ASM can do and how to do it. As a result, some implementations may have flaws or even bugs. The source code has been synchronized to Github if you have ideas, you can mention the issue.

Through a simple exploration of Gradle Transform + ASM, you can see that in the process of building the project, from the source (including Java /kotlin/ resource files/others) to the middle class and dex file to the final apK file generation. Many tasks are executed throughout the process. Between class and dex, you can still do a lot of articles using ASM.

Here we just print the log. In fact, we can collect some key information by piling, such as inserting the code of loading image URL into Glide, such as time statistics of key methods (such as onCreate method of Application). Sometimes what we care about is not necessarily the specific data, but the trend that the data shows. This information can be stored locally or even uploaded to the server in batches through code pilings to further analyze and disassemble key data later in the process.

Reference documentation

Android Gradle bytecode generation process

From Java bytecode to ASM practices

App fluency optimization: Use bytecode pilings to provide a quick tool for identifying time-consuming methods

ByteX