文章目录
- 一、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++;
}
});
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;
}
}
});
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;
}
}
});
使用ClickDebouncePlugin插件处理后的字节码的效果是:
在onClick事件中,加入了一个判断条件
if (DebouncedWarp.shouldDoClick(var1)) {
// 做原来的onClick事件
// doOriginalOnClick....
}
二、ClickDebouncePlugin插件原理介绍
2.1 准备好判断重复点击的工具类
准备好判断重复点击的工具类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插件
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
,
- 自定义属性
- 自定义一个extension属性为
time
,表示重复点击有效的间隔时间 - 自定义一个extension属性为
whitePackage
,表示 要白名单列表,这个可以给具体用插件的人来配置。
- 定义extension名称
上面表示 extension名称为"click_debounce"
所以在build.gradle中的配置如下所示:
// 应用插件
apply plugin: 'bytetea.click_debounce'
//配置插件的配置项
click_debounce {
enable true
enableInDebug true
// 间隔时间
time = 700
// 白名单包名
whitePackage = [
"android.x"
]
}
其中time
和whitePackage
来自于ClickDebouncePlugin
enable
和 enableInDebug
是来自于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")
}
}
上面的插件,会进行四步处理
- 判断当前遍历的
relativePath
是否等于net/mikaelzero/bytetea/lib/clickdebounce/DebouncedWarp.class
,如果等于的话,则通过TimeClassVisitor
来处理。 - 如果当前遍历的
relativePath
不等于net/mikaelzero/bytetea/lib/clickdebounce/DebouncedWarp.class
,则判断是否处于白名单中,如果不处于白名单中,则使用FindClickClassVisitor
处理 - 在
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
在MethodVistor
中访问LDC
指令的函数是visitLdcInsn
,
所以在这个函数下,我们判断下是否为Long
类型,然后进行代码修改。
- 如果
ClickDebouncePlugin
的Extension
配置的重复点击有效的间隔时间time
为空 则设置默认为300 - 否则 设置为
ClickDebouncePlugin
的Extension
配置的重复点击有效的间隔时间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;
}
}
上面的代码进行如下判断:
- 如果方法是
View.onClick()
方法,则使用ViewDebounceMethodVisitor
来处理。 - 如果是
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
对应的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();
}
这代码和 net.mikaelzero.bytetea.clickdebounce.visitor.ViewDebounceMethodVisitor#visitCode
方法的类似。
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插件,原理我们也讲解清楚了。
主要是干了几件事情
- 写一个检测android应用中是否对view在指定时间内重复点击的工具类
- 可以在extension中自定义间隔时间
- 通过判断View.onClick()事件使用ViewDebounceMethodVisitor来处理
- AdapterView的onItemClick() 事件使用AdapterViewDebounceMethodVisitor来处理
- ViewDebounceMethodVisitor和AdapterViewDebounceMethodVisitor的处理套路都是一样,就是将原来的点击事件代码中包裹在
if (DebouncedWarp.shouldDoClick(var1))
之中。
if (DebouncedWarp.shouldDoClick(var1)) {
// 原来的点击事件处理代码
}
大家可以通过原作者MikaelZero,
查看该ASM
插件ClickDebouncePlugin
的源代码,具体地址为:MikaelZero/ByteTea
- fork 添加注释版本: ouyangpeng/ByteTea
四、参考链接
- MikaelZero/ByteTea
- ByteX
- fork 添加注释版本: ouyangpeng/ByteTea