The foreword 0.

All of my original Android knowledge has been packaged up on GitHub. To create a series of senior junior high school engineers can understand the quality of the article, welcome to star~

Before reading this article, it is recommended that you have the following knowledge: Android packaging process +Gradle plugin +Java bytecode

In the Android Gradle Plugin, there is something called the Transform API(available from 1.5.0). The Transform API allows you to process.class files before they are converted to dex files. Like surveillance, burial sites, things like that.

For handling.class files, we use ASM.ASM is a generic Java bytecode manipulation and analysis framework. It can be used directly in binary form to modify existing classes or dynamically generate classes. Modifying the class directly by manipulating the bytecode during packaging has no impact on runtime performance, so it is quite efficient.

This article will give you a brief introduction to Transform and ASM, and finally practice using a small chestnut. Source address of demo

1. Use the Transform API

1.1 Register a custom Transform

Write a Plugin and register your custom Transform using the registerTransform method.

class MethodTimeTransformPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        // Registration method 1
        AppExtension appExtension = project.extensions.getByType(AppExtension)
        appExtension.registerTransform(new MethodTimeTransform())

        // Registration method 2
        //project.android.registerTransform(new MethodTimeTransform())}}Copy the code

Register the Transform through its registerTransform method by getting the Module’s Project’s AppExtension.

When registered here, a task is generated in the TransformManager#addTransform during compilation, and when executed, the Transform method of our custom Transform is executed. This task executes when the. Class file is converted to the. Dex file. The transformation logic is defined in the transform method.

1.2 Customize a Transform

Let’s look at the standard Transform template code:

class MethodTimeTransform extends Transform {

    @Override
    String getName() {
        return "MethodTimeTransform"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        // The data type to be processed
        return TransformManager.CONTENT_CLASS
    }

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

    @Override
    boolean isIncremental() {
        // Whether incremental compilation is supported
        return true
    }

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

        / / TransformOutputProvider management output paths, if consumer input is empty, is also empty outputProvider
        TransformOutputProvider outputProvider = transformInvocation.outputProvider

        / / transformInvocation. The type of inputs is Collection < TransformInput >, can obtain the jar package and class folder path. Output is needed for the next task
        transformInvocation.inputs.each { input -> // The input here is TransformInput

            input.jarInputs.each { jarInput ->
                / / handle the jar
                processJarInput(jarInput, outputProvider)
            }

            input.directoryInputs.each { directoryInput ->
                // Process the source file
                processDirectoryInput(directoryInput, outputProvider)
            }
        }
    }

    void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
        File dest = outputProvider.getContentLocation(jarInput.file.absolutePath, jarInput.contentTypes, jarInput.scopes, Format.JAR)
        // Copy the modified bytecode to dest to interfere with the bytecode during compilation
        println("Copy file $dest -----")
        FileUtils.copyFile(jarInput.file, dest)
    }

    void processDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
        File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format
                .DIRECTORY)
        // Copy the modified bytecode to dest to interfere with the bytecode during compilation
        println("Copy folder $dest -----")
        FileUtils.copyDirectory(directoryInput.file, dest)
    }

}
Copy the code
  1. getName()Said the current Transform name, the name will be used to create the directory, it will appear in the app/build/intermediates/transforms directories below.
  2. getInputTypes()The type of data that needs to be processed to determine which types of results we need to convert, such as class, resource file, etc.
    • CONTENT_CLASS: indicates that Java class files need to be processed
    • CONTENT_JARS: indicates the Java class and resource files that need to be processed
    • CONTENT_RESOURCES: indicates the Java resource file that needs to be processed
    • CONTENT_NATIVE_LIBS: represents code that needs to process native libraries
    • CONTENT_DEX: indicates that the DEX file needs to be processed
    • CONTENT_DEX_WITH_RESOURCES: indicates that DEX and Java resource files need to be processed
  3. getScopes(): represents the range of content the Transform will operate on (used in the demo aboveSCOPE_FULL_PROJECTIs a collection of scopes that containsScope.PROJECT.Scope.SUB_PROJECTS.Scope.EXTERNAL_LIBRARIESThese things, of course, there are other collections in the TransformManager that I won’t give you examples of).
    • PROJECT: Only PROJECT content
    • SUB_PROJECTS: Only subprojects
    • EXTERNAL_LIBRARIES: Only external libraries are available
    • TESTED_CODE: test code
    • PROVIDED_ONLY: Provides only local or remote dependencies
  4. isIncremental(): Whether incremental update is supported
    • If true is returned, TransformInput will contain a list of modified files
    • If false, full compilation is performed, deleting the last output
  5. transform(): Performs the specific conversion logic.
    • Consumer Transform: In the Transform method, we need to copy each JAR and class file to the dest path. This dest path is the input data for the next Transform. While copying, we can make some changes to the bytecode of the JAR and class files before copying. As you can see, if we register the Transform but don’t copy the content to the input path required for the next Transform, we will have problems, such as missing a few classes or something. In the demo above, all the input files are copied to the target directory, and no bytecode files are processed.
    • Referential Transform: The current Transform can read these inputs without output to the next Transform.

As you can see, the core code is inside the transform() method, and we need to make some bytecode changes to the class file to make the transform work.

That’s true, but can bytecode be changed if it wants to be? Those of you who forgot what bytecode is can review my previous post on Java Bytecode interpretation. Bytecode is complicated, and it’s very, very hard to even “read” it, and it’s even harder to change it.

Fortunately, you can easily modify bytecode with the ASM tool described below.

1.3 Incremental Compilation

Incremental() is the value returned by the isIncremental() method in the Transform. False means incremental compilation is not enabled and each file has to be processed every time, slowing compilation time very, very much. With this method, we can change the return value to true to enable incremental compilation. Of course, with incremental compilation enabled, you need to check the Status of each file, and then do different things based on the Status of that file.

The specific Status is as follows:

  • NOTCHANGED: The current file does not need to be processed, not even copied
  • ADDED: Normal processing and output to the next task
  • CHANGED: Normal processing, output to the next task
  • REMOVED: Removes files from the outputProvider obtaining path

To see how this works, let’s make a simple change to the above DMEO code:

@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
    super.transform(transformInvocation)
    printCopyRight()

    / / TransformOutputProvider management output paths, if consumer input is empty, is also empty outputProvider
    TransformOutputProvider outputProvider = transformInvocation.outputProvider

    // Incremental compilation is determined by the isIncremental method
    // Incremental() returns true when written by isIncremental() above. The value may not be true depending on the context. For example, the first run after clean is definitely not an incremental compile.
    boolean isIncremental = transformInvocation.isIncremental()
    if(! isIncremental) {// Delete all files before incremental compilation
        outputProvider.deleteAll()
    }

    / / transformInvocation. The type of inputs is Collection < TransformInput >, can obtain the jar package and class folder path. Output is needed for the next task
    transformInvocation.inputs.each { input -> // The input here is TransformInput

        input.jarInputs.each { jarInput ->
            / / handle the jar
            processJarInput(jarInput, outputProvider, isIncremental)
        }

        input.directoryInputs.each { directoryInput ->
            // Process the source file
            processDirectoryInput(directoryInput, outputProvider, isIncremental)
        }
    }
}

/** * process the jar * to copy the modified bytecode to dest, so that we can interfere with the bytecode during compilation */
void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider, boolean isIncremental) {
    def status = jarInput.status
    File dest = outputProvider.getContentLocation(jarInput.file.absolutePath, jarInput.contentTypes, jarInput.scopes, Format.JAR)
    if (isIncremental) {
        switch (status) {
            case Status.NOTCHANGED:
                break
            case Status.ADDED:
            case Status.CHANGED:
                transformJar(jarInput.file, dest)
                break
            case Status.REMOVED:
                if (dest.exists()) {
                    FileUtils.forceDelete(dest)
                }
                break}}else {
        transformJar(jarInput.file, dest)
    }

}

void transformJar(File jarInputFile, File dest) {
    //println(" $dest -----")
    FileUtils.copyFile(jarInputFile, dest)
}

/** * process the source file ** copy the modified bytecode to dest, so as to achieve the purpose of bytecode intervention during compilation */
void processDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider, boolean isIncremental) {
    File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format
            .DIRECTORY)
    FileUtils.forceMkdir(dest)

    println("isIncremental = $isIncremental")

    if (isIncremental) {
        String srcDirPath = directoryInput.getFile().getAbsolutePath()
        String destDirPath = dest.getAbsolutePath()
        Map<File, Status> fileStatusMap = directoryInput.getChangedFiles()
        for (Map.Entry<File, Status> changedFile : fileStatusMap.entrySet()) {
            Status status = changedFile.getValue()
            File inputFile = changedFile.getKey()
            String destFilePath = inputFile.getAbsolutePath().replace(srcDirPath, destDirPath)
            File destFile = new File(destFilePath)
            switch (status) {
                case Status.NOTCHANGED:
                    break
                case Status.ADDED:
                case Status.CHANGED:
                    FileUtils.touch(destFile)
                    transformSingleFile(inputFile, destFile)
                    break
                case Status.REMOVED:
                    if (destFile.exists()) {
                        FileUtils.forceDelete(destFile)
                    }
                    break}}}else {
        transformDirectory(directoryInput.file, dest)
    }
}

void transformSingleFile(File inputFile, File destFile) {
    println("Copy a single file")
    FileUtils.copyFile(inputFile, destFile)
}

void transformDirectory(File directoryInputFile, File dest) {
    println("Copy folder $dest -----")
    FileUtils.copyDirectory(directoryInputFile, dest)
}
Copy the code

Depending on whether the update is incremental, if not, all previous files are deleted. Then the state of each file is judged, according to its state to decide whether to delete, or copy. When incremental compilation is enabled, the speed increase is particularly significant.

1.4 Concurrent Compilation

After all, we are compiling on a computer, and although we are squeezing the performance of the computer, we are making concurrent compilation work. Easy to say, just a few lines of code

private WaitableExecutor mWaitableExecutor = WaitableExecutor.useGlobalSharedThreadPool()
transformInvocation.inputs.each { input -> // The input here is TransformInput

    input.jarInputs.each { jarInput ->
        / / handle the jar
        mWaitableExecutor.execute(new Callable<Object>() {
            @Override
            Object call() throws Exception {
                / / multi-threaded
                processJarInput(jarInput, outputProvider, isIncremental)
                return null}})}// Process the source file
    input.directoryInputs.each { directoryInput ->
        / / multi-threaded
        mWaitableExecutor.execute(new Callable<Object>() {
            @Override
            Object call() throws Exception {
                processDirectoryInput(directoryInput, outputProvider, isIncremental)
                return null}}}})// Wait for all tasks to finish
mWaitableExecutor.waitForTasksWithQuickFail(true)
Copy the code

Not much code has been added, but everything else is previous. That is, place the processing logic inside the thread to execute, and then wait until all the threads have finished processing the task.

This is the end of the basic Transform API, the principle (the system has a number of Transform to handle the logic of the class dex process, we can also customize the Transform to participate in the process, the Transform is actually executed in a Task). Now let’s see how to use ASM to modify bytecode to achieve cool features.

2. ASM

2.1 introduction

ASM website

ASM is a generic Java bytecode manipulation and analysis framework. It can be used directly in binary form to modify existing classes or dynamically generate classes. ASM provides common bytecode conversion and analysis algorithms from which you can build custom complex transformation and code analysis tools. ASM provides similar functionality to other Java bytecode frameworks, but focuses on performance. Because it is designed and implemented to be as small and as fast as possible, it is well suited for use in dynamic systems (but of course it can also be used statically, such as in compilers). (May not be very accurate translation, good English students can go to the official website to see the original words)

2.2 introduced the ASM

Below is the build.gradle configuration in buildSrc in my demo. It contains all dependencies for Plugin+Transform+ASM, so don’t worry.

dependencies {
    implementation gradleApi()
    implementation localGroovy()
    // Common IO operations
    implementation "Commons - the IO: the Commons - IO: 2.6"

    // Android DSL Android compiled most of the gradle source
    implementation 'com. Android. Tools. Build: gradle: 3.6.2'
    implementation 'com. Android. Tools. Build: gradle - API: 3.6.2'
    //ASM
    implementation 'org. Ow2. Asm: asm: 7.1'
    implementation 'org. Ow2. Asm: asm - util: 7.1'
    implementation 'org. Ow2. Asm: asm - Commons: 7.1'
}
Copy the code

2.3 Basic USE of ASM

Let’s look at some common objects before we use them

  • ClassReader: Parses the contents of the class file in the manner defined in the Java virtual Machine specification, calling the appropriate method in ClassVisitor when the appropriate field is encountered
  • ClassVisitor: A visitor to a class in Java that provides a list of methods to be called by a ClassReader. It is an abstract class that needs to be inherited when used.
  • ClassWriter: This is a class that inherits from ClassVisitor and is primarily responsible for writing the data passed from ClassReader to a byte stream. After the data is passed, the complete byte stream can be obtained through its toByteArray method.
  • ModuleVisitor: The visitor to a module in Java, as the return value of the ClassVisitor. VisitModule method, or null if you are not concerned with the usage of the module.
  • AnnotationVisitor: visitors to the annotations in Java, as ClassVisitor. VisitTypeAnnotation return values, don’t care about the usage of annotations can also return null.
  • FieldVisitor: a visitor to a field in Java, as returned by ClassVisitor. VisitField, or null regardless of field usage.
  • MethodVisitor: A visitor to a method in Java. As the return value of ClassVisitor. VisitMethod, null can be returned regardless of method usage.

I’m just going to take a look at these objects, just so they look familiar, and we’re going to use them.

The general workflow: A class bytecode file is read through a ClassReader, which then displays the data through a ClassVisitor(the ClassWriter above is actually a ClassVisitor). Presentation: Each detail of the bytecode is passed through the interface to the ClassVisitor in order. For example, if the xx method of the class file is accessed, the visitMethod method of the ClassVisitor is called back; When the properties of the class file are accessed, the visitField method of the ClassVisitor is called back.

ClassWriter is a class that inherits from ClassVisitor, which holds the byte stream data read by ClassReader and finally obtains the full byte stream through its toByteArray method.

Let’s write a simple way to copy a class file:

private void copyFile(File inputFile, File outputFile) {
    FileInputStream inputStream = new FileInputStream(inputFile)
    FileOutputStream outputStream = new FileOutputStream(outputFile)
    
    //1. Build the ClassReader object
    ClassReader classReader = new ClassReader(inputStream)
    //2. Construct the ClassVisitor implementation class ClassWriter
    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
    //3. Call back the contents read by ClassReader to the ClassVisitor interface
    classReader.accept(classWriter, ClassReader.EXPAND_FRAMES)
    //4. Get the full byte stream through the toByteArray method of the classWriter object
    outputStream.write(classWriter.toByteArray())

    inputStream.close()
    outputStream.close()
}
Copy the code

Now, some of you might be feeling a little bit of this. The ClassReader object is specifically responsible for reading the bytecode file, and ClassWriter is a class that inherits from the ClassVisitor, through which data is called back when the ClassReader reads the bytecode file. We can define a ClassWriter to receive the byte data that we read. At the same time, we can insert something in front of or behind the data. Finally, we can export the bytecode data through the ClassWriter’s toByteArray method and write it to a new file .

Now, for example, what is the use of a stake? To implement a simple requirement, insert a print Hello World at the beginning of each method. The code.

The code before modification is as follows:

private void test(a) {
    System.out.println("test");
}
Copy the code

Expected modified code:

private void test(a) {
    System.out.println("Hello World!");
    System.out.println("test");
}
Copy the code

Simply change the code for the copy file above

void traceFile(File inputFile, File outputFile) {
    FileInputStream inputStream = new FileInputStream(inputFile)
    FileOutputStream outputStream = new FileOutputStream(outputFile)

    ClassReader classReader = new ClassReader(inputStream)
    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
    classReader.accept(new HelloClassVisitor(classWriter)), ClassReader.EXPAND_FRAMES)
    outputStream.write(classWriter.toByteArray())

    inputStream.close()
    outputStream.close()
}
Copy the code

The only thing that has changed is the ClassVisitor object passed in to the Accept method of the classReader. We have a custom HelloClassVisitor.

class HelloClassVisitor extends ClassVisitor {

    HelloClassVisitor(ClassVisitor cv) {
        // You need to specify opcodes.asm7
        super(Opcodes.ASM7, cv)
    }

    @Override
    MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        def methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions)
        return new HelloMethodVisitor(api, methodVisitor, access, name, descriptor)
    }
}
Copy the code

We have a custom ClassVisitor that passes the ClassWriter into it. In a ClassVisitor implementation, functionality is delegated to the ClassVisitor object as soon as it is passed in. This ClassWriter that I pass in reads the bytecode, and toByteArray is all the bytecode. To no avail, look at the code:

public abstract class ClassVisitor {
    /** The class visitor to which this visitor must delegate method calls. May be null. */
  protected ClassVisitor cv;
  
   public ClassVisitor(final int api, final ClassVisitor classVisitor) {
    if(api ! = Opcodes.ASM7 && api ! = Opcodes.ASM6 && api ! = Opcodes.ASM5 && api ! = Opcodes.ASM4) {throw new IllegalArgumentException("Unsupported api " + api);
    }
    this.api = api;
    this.cv = classVisitor;
  }
  
  public AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) {
    if(cv ! =null) {
      return cv.visitAnnotation(descriptor, visible);
    }
    return null;
  }
  
  public MethodVisitor visitMethod(
      final int access,
      final String name,
      final String descriptor,
      final String signature,
      final String[] exceptions) {
    if(cv ! =null) {
      return cv.visitMethod(access, name, descriptor, signature, exceptions);
    }
    return null; }... }Copy the code

With the ClassWriter we pass in, we only need to focus on what needs to be modified when we customize the ClassVisitor. We want to pile in a method, so naturally we care about the visitMethod method, which will be called back when ClassReader reads the method in the class file. Here we first call the visitMethod method of ClassVisitor in the visitMethod of HelloClassVisitor to get the MethodVisitor object.

The MethodVisitor is similar to the ClassVisitor, in that the ClassReader calls back the visitParameter,visitAnnotationDefault, and VIS in the class when it reads the method ItAnnotation, and so on.

So in order to be able to pile the method, we need to wrap up another layer and implement the MethodVisitor ourselves. We pass the MethodVisitor returned by classWriter.visitMethod into the custom MethodVisitor and pile the method where it started. AdviceAdapter is a class that inherits from MethodVisitor, which conveniently callbacks method entry (onMethodEnter) and method exit (onMethodExit). We just need to insert the pile into the method entry, the onMethodEnter method.

class HelloMethodVisitor extends AdviceAdapter {

        HelloMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
            super(api, methodVisitor, access, name, descriptor)
        }

        // Method entry
        @Override
        protected void onMethodEnter() {
            super.onMethodEnter()
            // Mv here is MethodVisitor
            mv.visitFieldInsn(GETSTATIC, "java/lang/System"."out"."Ljava/io/PrintStream;");
            mv.visitLdcInsn("Hello World!");
            mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream"."println"."(Ljava/lang/String;) V".false); }}Copy the code

The core code for piling requires some core knowledge of bytecode, which is not covered here. I recommend reading the section on bytecode in Understanding the Java Virtual Machine.

Of course, there are shortcuts to writing this code quickly. Install an ASM Bytecode Outline plug-in, then write a random Test class, and then write a random method

public class Test {
    public void hello(a) {
        System.out.println("Hello World!"); }}Copy the code

Then select the test.java file, right-click the menu, and click Show ByteCode Outline

Select ASMified in right pane to get code like this:

mv = cw.visitMethod(ACC_PUBLIC, "hello", "()V", null, null); mv.visitCode(); Label l0 = new Label(); mv.visitLabel(l0); mv.visitLineNumber(42, l0); mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;" ); mv.visitLdcInsn("Hello World!" ); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;) V", false); Label l1 = new Label(); mv.visitLabel(l1); mv.visitLineNumber(43, l1); mv.visitInsn(RETURN); Label l2 = new Label(); mv.visitLabel(l2); mv.visitLocalVariable("this", "Lcom/xfhy/gradledemo/Test;" , null, l0, l2, 0); mv.visitMaxs(2, 1); mv.visitEnd();Copy the code

We don’t need labels, so we’re left with the core code

mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;" ); mv.visitLdcInsn("Hello World!" ); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;) V", false);Copy the code

At this point, the basic use of ASM comes to an end. ASM maneuvering is very strong, how bold people, how much production. You can achieve almost anything you want. It’s all about what you think. However, there is a small problem. The above plug-ins can only generate simple code. If you need to write complex logic, you have to go deep into Java bytecode to write it yourself or understand the ASM stub code.

3. ASM combat against quick click (jitter)

The little demo above prints out “Hello World!” in each method. It doesn’t seem to make any practical sense.. We decided to make something that actually makes sense, because normally, when we’re doing development, we’re trying to prevent people from clicking on a View very quickly. This is for the pursuit of better user experience. If not handled, two consecutive same interfaces may be opened when the user quickly clicks the Button, which is a little strange in the user’s view and affects the experience. So, in general, we’re going to do some limiting.

And when we do that, it’s actually very simple, we just take one of the quick click events and we just take one of the quick click events. What are the options for dealing with that? Here are a few that come to mind

  1. Check in the BaseActivity dispatchTouchEvent ifACTION_DOWN&& A quick click returns true.
  2. Write a utility class that records the time of the last click, and each time it’s a quick click, it’s a quick click, and if it is, it doesn’t respond to events.
  3. On the basis of scheme 2, the click time of each View can be recorded to control more accurately.

Here is a utility class I simply implemented, FastClickUtil.java

public class FastClickUtil {

    private static final int FAST_CLICK_TIME_DISTANCE = 300;
    private static long sLastClickTime = 0;

    public static boolean isFastDoubleClick(a) {
        long time = System.currentTimeMillis();
        long timeDistance = time - sLastClickTime;
        if (0 < timeDistance && timeDistance < FAST_CLICK_TIME_DISTANCE) {
            return true;
        }
        sLastClickTime = time;
        return false; }}Copy the code

With this utility class, you can insert an isFastDoubleClick() judgment at the beginning of each onClick method. Something like this:

public void onClick(View view) {
    if (!FastClickUtil.isFastDoubleClick()) {
        ......
    }
}
Copy the code

In order to achieve the final result above, all we really need to do is:

  1. Find the onClick method
  2. To insert the pile

The code is similar to the demo above except for the custom ClassVisitor. Let’s look at the custom ClassVisitor.

class FastClickClassVisitor extends ClassVisitor {

    FastClickClassVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM7, classVisitor)
    }

    @Override
    MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        def methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions)
        if (name == "onClick" && descriptor == "(Landroid/view/View;) V") {
            return new FastMethodVisitor(api, methodVisitor, access, name, descriptor)
        } else {
            return methodVisitor
        }
    }
}
Copy the code

Inside the visitMethod inside the ClassVisitor, just find the onClick method and customize your own MethodVisitor.

class FastMethodVisitor extends AdviceAdapter {

    FastMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
        super(api, methodVisitor, access, name, descriptor)
    }

    // Method entry
    @Override
    protected void onMethodEnter(a) {
        super.onMethodEnter()
        mv.visitMethodInsn(INVOKESTATIC, "com/xfhy/gradledemo/FastClickUtil"."isFastDoubleClick"."()Z".false)
        Label label = new Label()
        mv.visitJumpInsn(IFEQ, label)
        mv.visitInsn(RETURN)
        mv.visitLabel(label)
    }
}
Copy the code

Call the static FastClickUtil method isFastDoubleClick() inside the method entry (onMethodEnter()) to check. At this point, our small case calculation is complete. As you can see, using ASM, we can easily implement functionality that we had previously seen as cumbersome, but with a low level of intrusion, without having to change all the previous code.

After piling, you can drag the compiled APK directly into JADX to see the final source verification, or you can install the APK directly on the phone for verification.

Of course, there are a few less human aspects to this implementation. For example, some View click events do not need to be stabilized. How to do? It’s not appropriate to use the above method. Instead, you can create a custom annotation and annotate it on the onClick method that doesn’t need to deal with the stabilization. Then check with ASM that if an onClick method has this annotation, it will not be inserted. It worked out perfectly. I’m not going to do it with you, but I’ll leave it to you to practice after class.

reference

  • ASM website
  • AOP’s weapon: An introduction to ASM 3.0
  • Introduction and use of ASM library
  • ASM (Initial use)
  • Android Gradle Plugin packs the Transform API of Apk
  • Play with bytecode in your Android project
  • [Android] Gradle + ASM
  • Hunter