Everyone, can you give me a star for my project or my previous article? It’s so bitter. Github.com Nuggets article

background

The product wants to optimize for multiple quick clicks, and the desired effect is that double clicks won’t open multiple times

From a development point of view, I could use Kotlin’s extension method to fix this, but the historical debt would probably throw me off my feet, and the Java code would have problems. Is there any way to make the development can be opportunistic? I think of the way of staking and burying points mentioned in last year’s project. I wonder if I can solve this problem by weaving and inserting bytecode during compilation.

Introduction of the transform

In the packaging process, we know that after generating the.class file, we use the dx tool to generate the.dex file, and we use the Transform API to modify the.class file after generating the.class file, thus modifying the source code. We registered the Transform into AppExtension, which executes tasks of type Tramsform after Java Compile Task is executed.

Specific development

Initialize the

First create a Groovy Module and then initialize a Gradle plug-in.






Blog.csdn.net/a296777513/…


Build the transform

class DoubleTabTransform extends Transform {

    Project project

    DoubleTabTransform(Project project) {
        this.project = project
    }

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

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

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

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        final DoubleTapDelegate injectHelper = new DoubleTapDelegate()
        BaseTransform baseTransform = new BaseTransform(transformInvocation, new TransformCallBack() {

            @Override
            byte[] processJarClass(String className, byte[] classBytes, BaseTransform transform) {
                if (ClassUtils.checkClassName(className)) {
                    return injectHelper.transformByte(classBytes)
                } else {
                    return null
                }
            }

            @Override
            File processClass(File dir, File classFile, File tempDir, BaseTransform transform) {
                String absolutePath = classFile.absolutePath.replace(dir.absolutePath + File.separator, "")
                String className = ClassUtils.path2Classname(absolutePath)
                if (ClassUtils.checkClassName(className)) {
                    return injectHelper.beginTransform(className, classFile, transform.context.getTemporaryDir())
                } else {
                    return null
                }
            }
        })
        baseTransform.startTransform()
    }
}

Copy the code

The above code abstracts the Transform and ClassVisitor code for quick access if similar insertion logic is available. The main logical code is to scan jar packages and.class files, call back the modification method if the file meets the modification criteria, and then access the file based on the ASM ClassVisitor.

ClassVisitor mechanism

This can be seen on the web, so I won’t overstate it, but basically it’s a class accessor, and then it sequentially reads all the properties of the class, all the methods, and every line of the method.

class ClassFilterVisitor extends ClassVisitor {

    private String[] interfaces
    boolean visitedStaticBlock = false
    private String owner

    ClassFilterVisitor(ClassVisitor classVisitor) {
        super(Opcodes.ASM5, classVisitor)
    }

    @Override
    void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces)
        this.interfaces = interfaces
        if(interfaces ! =null && interfaces.length > 0) {
            for (Map.Entry<String, MethodCell> entry : MethodHelper.sInterfaceMethods.entrySet()) {
                MethodCell cell = entry.value
                if(cell ! =null && interfaces.contains(cell.parent)) {
                    visitedStaticBlock = true
                    this.owner = name
                    cv.visitField(Opcodes.ACC_PRIVATE + Opcodes.ACC_FINAL, "doubleTap"."Lcom/xxx/doubleclickplugin/sample/test/DoubleTapCheck;",
                            signature, null)}}}}@Override
    FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        return super.visitField(access, name, descriptor, signature, value)
    }

    @Override
    MethodVisitor visitMethod(int access, String name,
                              String desc, String signature, String[] exceptions) {
        if(interfaces ! =null && interfaces.length > 0) {
            try {
                if (visitedStaticBlock && name == "<init>") {
                    MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions)
                    return new InitBlockVisitor(methodVisitor, owner)
                }
                MethodCell cell = MethodHelper.sInterfaceMethods.get(name + desc)
                if(cell ! =null && interfaces.contains(cell.parent)) {
                    MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions)
                    CheckVisitor mv = new CheckVisitor(methodVisitor, owner)
                    return mv
                }
            } catch (Exception e) {
                e.printStackTrace()
            }
        }
        return super.visitMethod(access, name, desc, signature, exceptions)
    }

}

Copy the code

Class before modification

public class TestJavaClickListener implements View.OnClickListener {
    @Override
    public void onClick(View v) {
        Log.i("onClick"."1");
        Log.i("onClick"."2");
        Log.i("onClick"."3");
        Log.i("onClick"."4"); }}Copy the code

Modified classes

public class TestJavaClickListener implements OnClickListener {
    private final DoubleTapCheck doubleTap = new DoubleTapCheck();

    public TestJavaClickListener(a) {}public void onClick(View var1) {
        if (this.doubleTap.isNotDoubleTap()) {
            Log.i("onClick"."1");
            Log.i("onClick"."2");
            Log.i("onClick"."3");
            Log.i("onClick"."4"); }}}Copy the code

This is the class accessor within the project, where the visit method represents that the class has been accessed, and returns basic parameters such as the interface that the class inherits. I insert a reference index in this method, which simply declares a DoubleTapCheck doubleTap; And then there’s the visitMethod of ClassVistior, and this is the main thing that we need to fix, and one of the key points is that we need to fix two things, the initialization of the class, and the onClick method.

Init, we go back and initialize doubleTap, and InitBlockVisitor.

public class InitBlockVisitor extends MethodVisitor {
    private String owner;

    InitBlockVisitor(MethodVisitor mv, String owner) {
        super(Opcodes.ASM5, mv);
        this.owner = owner;
    }

    @Override
    public void visitInsn(int opcode) {
        if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)
                || opcode == Opcodes.ATHROW) {
            mv.visitVarInsn(Opcodes.ALOAD, 0);
            mv.visitTypeInsn(Opcodes.NEW, "com/xxxx/doubleclickplugin/sample/test/DoubleTapCheck");
            mv.visitInsn(Opcodes.DUP);
            mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/xxx/doubleclickplugin/sample/test/DoubleTapCheck"."<init>"."()V".false);
            mv.visitFieldInsn(Opcodes.PUTFIELD, owner, "doubleTap"."Lcom/xxx/doubleclickplugin/sample/test/DoubleTapCheck;");
        }
        super.visitInsn(opcode); }}Copy the code

The only thing this code does is execute new DoubleTapCheck() after init; This operation. Here I use an ASM plugin for IDEA asm ByteCode Viewer. With this class you can simply observe the ByteCode of the code you want to insert, and then use ASM to insert the code you want.

Finally, we modified the onClick method


public class CheckVisitor extends MethodVisitor {
    private String owner;

    CheckVisitor(MethodVisitor mv, String owner) {
        super(Opcodes.ASM5, mv);
        this.owner = owner;
    }

    @Override
    public void visitCode(a) {
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitFieldInsn(Opcodes.GETFIELD, owner, "doubleTap"."Lcom/xxx/doubleclickplugin/sample/test/DoubleTapCheck;");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/xxx/doubleclickplugin/sample/test/DoubleTapCheck"."isNotDoubleTap"."()Z".false);
        Label label = new Label();
        mv.visitJumpInsn(Opcodes.IFNE, label);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitLabel(label);
        super.visitCode(); }}Copy the code

The only thing it does is insert the doubleTap. IsNotDoubleTap () logical judgment at the front of the onClick method.

Conditional statement and label analysis

Here is a pegs bytecode for OnClickListener, and we will examine this class to see how label is used

public class com/xxx/doubleclickplugin/sample/TestJavaClickListener implements android/view/View$OnClickListener {

  // access flags 0x609
  public static abstract INNERCLASS android/view/View$OnClickListener android/view/View OnClickListener

  // access flags 0x12
  private final Lcom/xxx/doubleclickplugin/sample/test/DoubleTapCheck; doubleTap

  // access flags 0x1
  public <init>()V
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    ALOAD 0
    NEW com/xxx/doubleclickplugin/sample/test/DoubleTapCheck
    DUP
    INVOKESPECIAL com/xxx/doubleclickplugin/sample/test/DoubleTapCheck.<init> ()V
    PUTFIELD com/xxx/doubleclickplugin/sample/TestJavaClickListener.doubleTap : Lcom/xxx/doubleclickplugin/sample/test/DoubleTapCheck;
    RETURN
    MAXSTACK = 3
    MAXLOCALS = 1

  // access flags 0x1
  publiconClick(Landroid/view/View;) V ALOAD0
    GETFIELD com/xxx/doubleclickplugin/sample/TestJavaClickListener.doubleTap : Lcom/xxx/doubleclickplugin/sample/test/DoubleTapCheck;
    INVOKEVIRTUAL com/xxx/doubleclickplugin/sample/test/DoubleTapCheck.isNotDoubleTap ()Z
    IFNE L0
    RETURN
   L0
    LDC "onClick"
    LDC "1"INVOKESTATIC android/util/Log.i (Ljava/lang/String; Ljava/lang/String;) I POP LDC"onClick"
    LDC "2"INVOKESTATIC android/util/Log.i (Ljava/lang/String; Ljava/lang/String;) I POP LDC"onClick"
    LDC "3"INVOKESTATIC android/util/Log.i (Ljava/lang/String; Ljava/lang/String;) I POP LDC"onClick"
    LDC "4"INVOKESTATIC android/util/Log.i (Ljava/lang/String; Ljava/lang/String;) I POP RETURN MAXSTACK =2
    MAXLOCALS = 2
}

Copy the code

Let’s start with line 24. First we get the 0 position which is the View, then we get the doubleTap instance and call the doubleTab. IsNotDoubleTap method. Line 27 is the key, judging the result of isNotDoubleTap and then jumping to the method block below. There is an L0 at the end of it, which I couldn’t understand at first. Finally, after using Javap to parse the bytecode, I found that this L0 actually maps to the L0 of the method block below, and in real bytecode, this is the corresponding line number. And that’s the Label Label that we use, so the Label Label, as its name implies, is the number of lines that mark a method block. It’s the number of lines of code between two labels.

Making a link