文章目录

  • 一、ClickDebouncePlugin插件介绍
  • 1.1 功能
  • 1.2 用法
  • 1.3 运行效果
  • 1.3.1 运行效果讲解
  • 1.3.2 实际运行效果分析
  • 1.3.2.1 源代码
  • 1.3.2.2 原始字节码
  • 1.3.2.3 使用ClickDebouncePlugin插件处理后的字节码
  • 二、ClickDebouncePlugin插件原理介绍
  • 2.1 准备好判断重复点击的工具类
  • 2.2 基于ByteX来编写ClickDebouncePlugin插件
  • 2.2.1 插件配置
  • 2.2.1.1 属性文件:`src\main\resources\META-INF\gradle-plugins\bytetea.click_debounce.properties`
  • 2.2.1.2 ClickDebounceExtension
  • 2.2.2 ClickDebouncePlugin具体实现类
  • 2.2.3 TimeClassVisitor
  • 2.2.4 TimeMethodVisitor
  • 2.2.5 FindClickClassVisitor
  • 2.2.6 Utils
  • 2.2.6 ViewDebounceMethodVisitor
  • 2.2.7 AdapterViewDebounceMethodVisitor
  • 三、总结
  • 四、参考链接



文章

【我的ASM学习进阶之旅】 如何基于ByteX快速上手,开发你自己的ASM插件?


中介绍了如何基于ByteX快速上手,

现在介绍一个基于ByteX开发的ASM插件ClickDebouncePlugin插件

一、ClickDebouncePlugin插件介绍

1.1 功能

ClickDebouncePlugin插件的功能是:处理Android的View在指定时间内重复点击的情况。

1.2 用法

在项目的根目录下的 build.gradle 文件中添加该插件的对应配置

classpath "net.mikaelzero.bytetea:click-debounce-plugin:$byteTeaVersion"

在对应的module的build.gradle 文件中添加该插件的对应配置

//应用插件
apply plugin: 'bytex'
apply plugin: 'bytetea.click_debounce'

//配置插件的配置项
click_debounce {
    enable true
    enableInDebug true
    // 间隔时间
    time = 700
    // 白名单包名
    whitePackage = [
            "android.x"
    ]
}

1.3 运行效果

1.3.1 运行效果讲解

处理之前 :

testTv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                testTv.setText(String.valueOf(num));
                num++;
            }
        });

处理之后 :

testTv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (DebouncedWarp.shouldDoClick(v)){
                    testTv.setText(String.valueOf(num));
                    num++;
                }
            }
        });

1.3.2 实际运行效果分析

1.3.2.1 源代码

src\main\java\net\mikaelzero\app\MainActivity.java

中有一个TextView设置点击事件

final TextView testTv = findViewById(R.id.testTv);
        testTv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                testTv.setText(String.valueOf(num));
                num++;
            }
        });

android 怎么解决重复引用问题 android重复点击_android 怎么解决重复引用问题

1.3.2.2 原始字节码

编译之后的原始字节码如下所示:
路径为:build\intermediates\javac\debug\classes\net\mikaelzero\app\MainActivity.class

  • javac 编译后的字节码
final TextView testTv = (TextView)this.findViewById(2131231052);
        testTv.setOnClickListener(new OnClickListener() {
            public void onClick(View var1) {
                if (DebouncedWarp.shouldDoClick(var1)) {
                    testTv.setText(String.valueOf(MainActivity.this.num));
                    ++MainActivity.this.num;
                }
            }
        });

android 怎么解决重复引用问题 android重复点击_java_02

1.3.2.3 使用ClickDebouncePlugin插件处理后的字节码

使用ClickDebouncePlugin插件处理后的字节码如下所示:
路径为:build\intermediates\transforms\ByteX\debug\44\net\mikaelzero\app\MainActivity.class

final TextView testTv = (TextView)this.findViewById(2131231052);
        testTv.setOnClickListener(new OnClickListener() {
            public void onClick(View var1) {
                if (DebouncedWarp.shouldDoClick(var1)) {
                    testTv.setText(String.valueOf(MainActivity.this.num));
                    ++MainActivity.this.num;
                }
            }
        });

android 怎么解决重复引用问题 android重复点击_字节码_03


使用ClickDebouncePlugin插件处理后的字节码的效果是:

在onClick事件中,加入了一个判断条件

if (DebouncedWarp.shouldDoClick(var1)) {
     // 做原来的onClick事件
     //	doOriginalOnClick....
}

二、ClickDebouncePlugin插件原理介绍

2.1 准备好判断重复点击的工具类

android 怎么解决重复引用问题 android重复点击_字节码_04


准备好判断重复点击的工具类DebouncedWarp,并打包成一个AAR库。

src\main\java\net\mikaelzero\bytetea\lib\clickdebounce\DebouncedWarp.java 源代码如下所示:

package net.mikaelzero.bytetea.lib.clickdebounce;

import android.view.View;

import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.TimeUnit;

/**
 * @Author: MikaelZero
 * @CreateDate: 2020/7/3 2:38 PM
 * @Description: DebouncedWarp
 */
public class DebouncedWarp {

    public static long FROZEN_WINDOW_MILLIS = 1000L;

    private static final String TAG = DebouncedWarp.class.getSimpleName();

    private static final Map<View, FrozenView> viewWeakHashMap = new WeakHashMap<>();

    public static boolean shouldDoClick(View targetView) {

        FrozenView frozenView = viewWeakHashMap.get(targetView);
        final long now = now();

        if (frozenView == null) {
            frozenView = new FrozenView(targetView);
            frozenView.setFrozenWindow(now + FROZEN_WINDOW_MILLIS);
            viewWeakHashMap.put(targetView, frozenView);
            return true;
        }

        if (now >= frozenView.getFrozenWindowTime()) {
            frozenView.setFrozenWindow(now + FROZEN_WINDOW_MILLIS);
            return true;
        }

        return false;
    }

    private static long now() {
        return TimeUnit.NANOSECONDS.toMillis(System.nanoTime());
    }

    private static class FrozenView extends WeakReference<View> {
        private long frozenWindowTime;

        FrozenView(View referent) {
            super(referent);
        }

        long getFrozenWindowTime() {
            return frozenWindowTime;
        }

        void setFrozenWindow(long expirationTime) {
            this.frozenWindowTime = expirationTime;
        }
    }
}

2.2 基于ByteX来编写ClickDebouncePlugin插件

android 怎么解决重复引用问题 android重复点击_字节码_05

2.2.1 插件配置

2.2.1.1 属性文件:src\main\resources\META-INF\gradle-plugins\bytetea.click_debounce.properties

  • 确定插件实现类
    首先 该文件定义插件的具体实现类为net.mikaelzero.bytetea.clickdebounce.ClickDebouncePlugin
implementation-class=net.mikaelzero.bytetea.clickdebounce.ClickDebouncePlugin
  • 确定插件名称
    并且该文件名称为bytetea.click_debounce.properties,所以插件的名称是bytetea.click_debounce

2.2.1.2 ClickDebounceExtension

package net.mikaelzero.bytetea.clickdebounce;

import com.ss.android.ugc.bytex.common.BaseExtension;

import java.util.List;

public class ClickDebounceExtension extends BaseExtension {

    /**
     * 重复点击有效的间隔时间
     */
    private long time;
    /**
     * 白名单
     */
    private List<String> whitePackage;

    public long getTime() {
        return time;
    }

    public void setTime(long time) {
        this.time = time;
    }

    public List<String> getWhitePackage() {
        return whitePackage;
    }

    public void setWhitePackage(List<String> whitePackage) {
        this.whitePackage = whitePackage;
    }

    @Override
    public String getName() {
        return "click_debounce";
    }
}

上面代码ClickDebounceExtension 继承自BaseExtension

  • 自定义属性
  1. 自定义一个extension属性为time,表示重复点击有效的间隔时间
  2. 自定义一个extension属性为whitePackage,表示 要白名单列表,这个可以给具体用插件的人来配置。
  • 定义extension名称
    上面表示 extension名称为 "click_debounce"

所以在build.gradle中的配置如下所示:

// 应用插件
apply plugin: 'bytetea.click_debounce'

//配置插件的配置项
click_debounce {
    enable true
    enableInDebug true
    // 间隔时间
    time = 700
    // 白名单包名
    whitePackage = [
            "android.x"
    ]
}

其中timewhitePackage来自于ClickDebouncePluginenableenableInDebug 是来自于ClickDebouncePlugin 继承的BaseExtension

2.2.2 ClickDebouncePlugin具体实现类

package net.mikaelzero.bytetea.clickdebounce

import com.android.build.gradle.AppExtension
import com.ss.android.ugc.bytex.common.CommonPlugin
import com.ss.android.ugc.bytex.common.visitor.ClassVisitorChain
import net.mikaelzero.bytetea.clickdebounce.visitor.FindClickClassVisitor
import net.mikaelzero.bytetea.clickdebounce.visitor.TimeClassVisitor
import org.gradle.api.Project
import javax.annotation.Nonnull

class ClickDebouncePlugin : CommonPlugin<ClickDebounceExtension?, Context>() {
    override fun getContext(project: Project, android: AppExtension, extension: ClickDebounceExtension?): Context {
        return Context(project, android, extension)
    }

    //relativePath = xxx.class
    override fun transform(@Nonnull relativePath: String, @Nonnull chain: ClassVisitorChain): Boolean {
        if (relativePath == "net/mikaelzero/bytetea/lib/clickdebounce/DebouncedWarp.class") {
            chain.connect(TimeClassVisitor(extension))
        } else {
            // 如果不是处于白名单中,则处理click事件
            if (!extension!!.whitePackage.any { relativePath.startsWith(it.replace(".", "/")) }) {
                chain.connect(FindClickClassVisitor())
            }
        }
        return super.transform(relativePath, chain)
    }

    override fun onApply(@Nonnull project: Project) {
        super.onApply(project)
        project.dependencies.add("implementation", "net.mikaelzero.bytetea:click-debounce-lib:1.0")
    }
}

上面的插件,会进行四步处理

  1. 判断当前遍历的relativePath是否等于net/mikaelzero/bytetea/lib/clickdebounce/DebouncedWarp.class,如果等于的话,则通过TimeClassVisitor来处理。
  2. 如果当前遍历的relativePath不等于net/mikaelzero/bytetea/lib/clickdebounce/DebouncedWarp.class,则判断是否处于白名单中,如果不处于白名单中,则使用FindClickClassVisitor处理
  3. onApply方法中声明当前插件依赖于AAR库"net.mikaelzero.bytetea:click-debounce-lib:1.0",即我们2.1章说的判断重复点击的工具类。

2.2.3 TimeClassVisitor

我们来看看第一步判断当前遍历的relativePath是否等于net/mikaelzero/bytetea/lib/clickdebounce/DebouncedWarp.class,如果等于的话,则通过TimeClassVisitor来处理。

TimeClassVisitor源代码如下所示:

package net.mikaelzero.bytetea.clickdebounce.visitor;

import com.ss.android.ugc.bytex.common.visitor.BaseClassVisitor;

import net.mikaelzero.bytetea.clickdebounce.ClickDebounceExtension;

import org.objectweb.asm.MethodVisitor;

public class TimeClassVisitor extends BaseClassVisitor {
    private ClickDebounceExtension clickDebounceExtension;

    public TimeClassVisitor(ClickDebounceExtension clickDebounceExtension) {
        this.clickDebounceExtension = clickDebounceExtension;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        mv = new TimeMethodVisitor(mv, clickDebounceExtension);
        return mv;
    }
}

上面的代码,在visitMethod方法中直接通过TimeMethodVisitor类来处理,并且将clickDebounceExtension传给了TimeMethodVisitor类。

2.2.4 TimeMethodVisitor

我们来看一看TimeMethodVisitor类的源代码,如下所示:

package net.mikaelzero.bytetea.clickdebounce.visitor;


import net.mikaelzero.bytetea.clickdebounce.ClickDebounceExtension;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class TimeMethodVisitor extends MethodVisitor {
    private ClickDebounceExtension clickDebounceExtension;

    public TimeMethodVisitor(MethodVisitor mv, ClickDebounceExtension clickDebounceExtension) {
        super(Opcodes.ASM5, mv);
        this.clickDebounceExtension = clickDebounceExtension;
    }


    @Override
    public void visitLdcInsn(Object value) {
        // 判断是否是Long型
        if (value instanceof Long) {
            // 如果  重复点击有效的间隔时间  为空   则设置默认为300
            if (clickDebounceExtension.getTime() == 0L) {
                super.visitLdcInsn(300L);
            } else {
                // 否则 设置为 gradle插件中配置的数值
                super.visitLdcInsn(clickDebounceExtension.getTime());
            }
        } else {
            super.visitLdcInsn(value);
        }
    }
}

我们通过 Jclasslib 插件可以知道,long类型在字节码中的指令都是LDC指令,因此只需要在访问LDC指令的时候进行修改即可。

因为DebouncedWarp中的FROZEN_WINDOW_MILLIS 赋值代码

public static long FROZEN_WINDOW_MILLIS = 1000L;

对应的字节码指令为

LDC 1000
PUTSTATIC net/mikaelzero/bytetea/lib/clickdebounce/DebouncedWarp.FROZEN_WINDOW_MILLIS : J

android 怎么解决重复引用问题 android重复点击_字节码_06

MethodVistor中访问LDC指令的函数是visitLdcInsn,
所以在这个函数下,我们判断下是否为Long类型,然后进行代码修改。

  1. 如果 ClickDebouncePluginExtension配置的重复点击有效的间隔时间 time 为空 则设置默认为300
  2. 否则 设置为 ClickDebouncePluginExtension配置的重复点击有效的间隔时间 time的数值,比如我们的demo设置为700

这样就可以把 net.mikaelzero.bytetea.lib.clickdebounce.DebouncedWarp#FROZEN_WINDOW_MILLIS的值通过gradle中的plugin extension 动态进行配置。

2.2.5 FindClickClassVisitor

上面的TimeClassVisitor把net/mikaelzero/bytetea/lib/clickdebounce/DebouncedWarp.class处理完毕,动态设置好了间隔时间后,我们要处理其他的class文件了。

我们来看看FindClickClassVisitor类,源代码如下所示:

package net.mikaelzero.bytetea.clickdebounce.visitor;

import com.ss.android.ugc.bytex.common.visitor.BaseClassVisitor;

import net.mikaelzero.bytetea.clickdebounce.Utils;

import org.objectweb.asm.MethodVisitor;

public class FindClickClassVisitor extends BaseClassVisitor {
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
        // 处理View.onClick()事件
        if (Utils.isViewOnclickMethod(access, name, desc)) {
            mv = new ViewDebounceMethodVisitor(mv);
        }
        // 处理 AdapterView的onItemClick() 事件
        if (Utils.isAdapterViewOnItemOnclickMethod(access, name, desc)) {
            mv = new AdapterViewDebounceMethodVisitor(mv);
        }
        return mv;
    }

}

上面的代码进行如下判断:

  1. 如果方法是 View.onClick() 方法,则使用ViewDebounceMethodVisitor来处理。
  2. 如果是Adapter中的onItemClick() 方法,则使用AdapterViewDebounceMethodVisitor来处理。

2.2.6 Utils

Utils.isViewOnclickMethod和Utils.isAdapterViewOnItemOnclickMethod是判断是否是某种点击事件,具体源代码如下所示:

package net.mikaelzero.bytetea.clickdebounce;


import org.objectweb.asm.Opcodes;


public class Utils implements Opcodes {

    /*
     *  & 是位运算bai里的位与操作,运算规则为
     *  0 & 0 = 0,
     *  0 & 1 = 0,
     *  1 & 0 = 0,
     *  1 & 1 = 1
     */

    private Utils() {
        throw new AssertionError("no instance");
    }

    public static boolean isPrivate(int access) {
        return (access & ACC_PRIVATE) != 0;
    }

    public static boolean isPublic(int access) {
        return (access & ACC_PUBLIC) != 0;
    }

    static boolean isStatic(int access) {
        return (access & ACC_STATIC) != 0;
    }

    public static boolean isAbstract(int access) {
        return (access & ACC_ABSTRACT) != 0;
    }

    public static boolean isbridge(int access) {
        return (access & ACC_BRIDGE) != 0;
    }

    public static boolean isSynthetic(int access) {
        return (access & ACC_SYNTHETIC) != 0;
    }

    public static boolean isViewOnclickMethod(int access, String name, String desc) {
        return (Utils.isPublic(access) && !Utils.isStatic(access) && !isAbstract(access))
                && name.equals("onClick") //
                && desc.equals("(Landroid/view/View;)V");
    }

    public static boolean isAdapterViewOnItemOnclickMethod(int access, String name, String desc) {
        return (Utils.isPublic(access) && !Utils.isStatic(access) && !isAbstract(access)) &&
                name.equals("onItemClick") && //
                desc.equals("(Landroid/widget/AdapterView;Landroid/view/View;IJ)V");
    }

}

2.2.6 ViewDebounceMethodVisitor

我们来看看 ViewDebounceMethodVisitor 怎么处理 View.onClick() 方法,代码如下所示:

package net.mikaelzero.bytetea.clickdebounce.visitor;


import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.IFNE;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.RETURN;

public class ViewDebounceMethodVisitor extends MethodVisitor {
    public ViewDebounceMethodVisitor(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        mv.visitVarInsn(ALOAD, 1);
        mv.visitMethodInsn(INVOKESTATIC, "net/mikaelzero/bytetea/lib/clickdebounce/DebouncedWarp",
                "shouldDoClick", "(Landroid/view/View;)Z", false);
        Label label = new Label();
        mv.visitJumpInsn(IFNE, label);
        mv.visitInsn(RETURN);
        mv.visitLabel(label);
    }
}

其实上面的代码 就是生成下面的代码

if (DebouncedWarp.shouldDoClick(var1)) {

 }

完整的onClick 添加的代码

public void onClick(View var1) {
        if (DebouncedWarp.shouldDoClick(var1)) {

        }
    }

这段代码对应的字节码为:

// access flags 0x1
  public onClick(Landroid/view/View;)V
   L0
    LINENUMBER 39 L0
    ALOAD 1
    INVOKESTATIC net/mikaelzero/bytetea/lib/clickdebounce/DebouncedWarp.shouldDoClick (Landroid/view/View;)Z
    IFEQ L1
   L1
    LINENUMBER 42 L1
   FRAME SAME
    RETURN
   L2
    LOCALVARIABLE this Lnet/mikaelzero/app/Test; L0 L2 0
    LOCALVARIABLE var1 Landroid/view/View; L0 L2 1
    MAXSTACK = 1
    MAXLOCALS = 2

android 怎么解决重复引用问题 android重复点击_字节码_07


对应的ASMified内容如下所示:

{
            mv = cw.visitMethod(ACC_PUBLIC, "onClick", "(Landroid/view/View;)V", null, null);
            mv.visitCode();
            Label l0 = new Label();
            mv.visitLabel(l0);
            mv.visitLineNumber(39, l0);
            mv.visitVarInsn(ALOAD, 1);
            mv.visitMethodInsn(INVOKESTATIC, "net/mikaelzero/bytetea/lib/clickdebounce/DebouncedWarp", "shouldDoClick", "(Landroid/view/View;)Z", false);
            Label l1 = new Label();
            mv.visitJumpInsn(IFEQ, l1);
            mv.visitLabel(l1);
            mv.visitLineNumber(42, l1);
            mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
            mv.visitInsn(RETURN);
            Label l2 = new Label();
            mv.visitLabel(l2);
            mv.visitLocalVariable("this", "Lnet/mikaelzero/app/Test;", null, l0, l2, 0);
            mv.visitLocalVariable("var1", "Landroid/view/View;", null, l0, l2, 1);
            mv.visitMaxs(1, 2);
            mv.visitEnd();
        }

android 怎么解决重复引用问题 android重复点击_字节码_08

这代码和 net.mikaelzero.bytetea.clickdebounce.visitor.ViewDebounceMethodVisitor#visitCode方法的类似。

android 怎么解决重复引用问题 android重复点击_android 怎么解决重复引用问题_09

2.2.7 AdapterViewDebounceMethodVisitor

处理 AdapterView的onItemClick() 事件的 AdapterViewDebounceMethodVisitor源代码如下所示:

package net.mikaelzero.bytetea.clickdebounce.visitor;


import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.IFNE;
import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.RETURN;

public class AdapterViewDebounceMethodVisitor extends MethodVisitor {
    public AdapterViewDebounceMethodVisitor(MethodVisitor mv) {
        super(Opcodes.ASM5, mv);
    }

    @Override
    public void visitCode() {
        super.visitCode();
        mv.visitVarInsn(ALOAD, 2);
        mv.visitMethodInsn(INVOKESTATIC, "net/mikaelzero/bytetea/lib/clickdebounce/DebouncedWarp",
                "shouldDoClick", "(Landroid/view/View;)Z", false);
        Label label = new Label();
        mv.visitJumpInsn(IFNE, label);
        mv.visitInsn(RETURN);
        mv.visitLabel(label);
    }
}

代码和ViewDebounceMethodVisitor类似,这里就不再重复讲解。

三、总结

好了,通过上面的步骤,我们就写好了这个检测android应用中是否对view在指定时间内重复点击的ASM插件,原理我们也讲解清楚了。

主要是干了几件事情

  1. 写一个检测android应用中是否对view在指定时间内重复点击的工具类
  2. 可以在extension中自定义间隔时间
  3. 通过判断View.onClick()事件使用ViewDebounceMethodVisitor来处理
  4. AdapterView的onItemClick() 事件使用AdapterViewDebounceMethodVisitor来处理
  5. ViewDebounceMethodVisitor和AdapterViewDebounceMethodVisitor的处理套路都是一样,就是将原来的点击事件代码中包裹在 if (DebouncedWarp.shouldDoClick(var1)) 之中。
if (DebouncedWarp.shouldDoClick(var1)) {
     // 原来的点击事件处理代码
 }

大家可以通过原作者MikaelZero

查看该ASM插件ClickDebouncePlugin的源代码,具体地址为:MikaelZero/ByteTea

四、参考链接