1.下载资源,导入到项目。
2.在你项目中创建一个FastClickHelper,比如
package com.csii.baseutil;
import android.view.View;
import com.csii.baseutil.utils.BaseLogUtil;
public class FastClickHelper {
private static final String TAG = "FastClickHelper";
private static final long DEFAULT_INTERVAL_MS = 300L; // 默认防抖间隔500毫秒
private static final int DEBOUNCE_WINDOW_MS = 30; // 10毫秒内的重复点击视为同一次
private static final int TAG_LAST_TIME = R.id.fastclick_last_time;
private static final int TAG_CUSTOM_INTERVAL = R.id.fastclick_custom_interval;
private static final int TAG_DISABLE = R.id.fastclick_disabled;
private static final int TAG_WHITELIST = R.id.fastclick_dispatch_white;
private static volatile boolean sEmergencyDisable = false;
/**
* 判断是否为快速点击
* @param view 被点击的View
* @return true-是快速点击(需要拦截),false-不是快速点击(允许执行)
*/
public static boolean isFastClick(View view) {
// 紧急降级
if (sEmergencyDisable) {
BaseLogUtil.log(TAG, "紧急降级模式已开启,放行所有点击");
return false;
}
if (view == null) {
BaseLogUtil.log(TAG, "View为空,放行点击");
return false;
}
// 检查白名单
try {
Boolean whitelist = (Boolean) view.getTag(TAG_WHITELIST);
if (whitelist != null && whitelist) {
BaseLogUtil.log(TAG, "View在白名单中,放行点击");
return false;
}
} catch (Exception e) {
BaseLogUtil.log(TAG, "读取白名单标签失败", e);
}
// 检查是否禁用防抖
try {
Boolean disabled = (Boolean) view.getTag(TAG_DISABLE);
if (disabled != null && disabled) {
BaseLogUtil.log(TAG, "防抖功能已禁用,放行点击");
return false;
}
} catch (Exception e) {
BaseLogUtil.log(TAG, "读取禁用标签失败", e);
}
// 获取自定义间隔时间
long interval = DEFAULT_INTERVAL_MS;
try {
Long customInterval = (Long) view.getTag(TAG_CUSTOM_INTERVAL);
if (customInterval != null && customInterval > 0) {
interval = customInterval;
BaseLogUtil.log(TAG, "使用自定义间隔时间: " + interval + "ms");
}
} catch (Exception e) {
BaseLogUtil.log(TAG, "读取自定义间隔时间失败", e);
}
// 获取上次点击时间
Long lastTime = null;
try {
lastTime = (Long) view.getTag(TAG_LAST_TIME);
} catch (Exception e) {
BaseLogUtil.log(TAG, "读取上次点击时间失败", e);
try {
view.setTag(TAG_LAST_TIME, null);
} catch (Exception ignored) {
}
return false;
}
long currentTime = System.currentTimeMillis();
// 防御:时间异常处理
if (lastTime != null) {
if (lastTime > currentTime || currentTime - lastTime > 30000) {
BaseLogUtil.log(TAG, "检测到异常的上次点击时间: " + lastTime + ",正在重置");
lastTime = null;
try {
view.setTag(TAG_LAST_TIME, null);
} catch (Exception ignored) {
return false;
}
}
}
// 首次点击
if (lastTime == null) {
try {
view.setTag(TAG_LAST_TIME, currentTime);
BaseLogUtil.log(TAG, "首次点击,放行");
} catch (Exception e) {
BaseLogUtil.log(TAG, "保存上次点击时间失败", e);
}
return false;
}
long diff = currentTime - lastTime;
// 10毫秒内的重复点击 → 拦截(视为插桩导致的重复调用)
if (diff < DEBOUNCE_WINDOW_MS) {
BaseLogUtil.log(TAG, "【拦截】10毫秒内重复点击,时间差=" + diff + "ms,视为同一次点击");
return false;
}
// 间隔不足设定时间 → 拦截(用户快速点击)
if (diff < interval) {
BaseLogUtil.log(TAG, "【拦截】点击过快,时间差=" + diff + "ms < " + interval + "ms");
return true;
}
// 正常点击,放行并更新时间
try {
view.setTag(TAG_LAST_TIME, currentTime);
BaseLogUtil.log(TAG, "【放行】正常点击,时间差=" + diff + "ms >= " + interval + "ms");
} catch (Exception e) {
BaseLogUtil.log(TAG, "更新上次点击时间失败", e);
}
return false;
}
/**
* 紧急降级开关(可在Application中调用)
* @param disable true-关闭防抖功能,false-开启防抖功能
*/
public static void setEmergencyDisable(boolean disable) {
sEmergencyDisable = disable;
BaseLogUtil.log(TAG, "紧急降级开关已设置为: " + (disable ? "关闭防抖" : "开启防抖"));
}
/**
* 清除某个View的所有防抖状态
* @param view 需要清除状态的View
*/
public static void clearViewState(View view) {
if (view != null) {
try {
view.setTag(TAG_LAST_TIME, null);
view.setTag(TAG_CUSTOM_INTERVAL, null);
view.setTag(TAG_DISABLE, null);
view.setTag(TAG_WHITELIST, null);
BaseLogUtil.log(TAG, "已清除View的防抖状态");
} catch (Exception ignored) {
BaseLogUtil.log(TAG, "清除View状态失败");
}
}
}
/**
* 设置自定义间隔(毫秒)
* @param view 目标View
* @param intervalMs 间隔时间(毫秒),必须大于0
*/
public static void setCustomInterval(View view, long intervalMs) {
if (view != null && intervalMs > 0) {
view.setTag(TAG_CUSTOM_INTERVAL, intervalMs);
BaseLogUtil.log(TAG, "设置自定义间隔时间: " + intervalMs + "ms");
}
}
/**
* 禁止某个View的防抖功能
* @param view 目标View
* @param disabled true-禁用防抖,false-启用防抖
*/
public static void setDisabled(View view, boolean disabled) {
if (view != null) {
view.setTag(TAG_DISABLE, disabled);
BaseLogUtil.log(TAG, "设置防抖功能: " + (disabled ? "禁用" : "启用"));
}
}
/**
* 将View加入白名单(白名单中的View不受防抖限制)
* @param view 目标View
* @param whitelist true-加入白名单,false-移除白名单
*/
public static void setWhitelist(View view, boolean whitelist) {
if (view != null) {
view.setTag(TAG_WHITELIST, whitelist);
BaseLogUtil.log(TAG, "设置白名单: " + (whitelist ? "已加入" : "已移除"));
}
}
}
3.修改doubleClick lib包下的FastClickMethodVisitor文件中 修改为自己的包名,就是FastClickHelper文件的包名
package com.csii.doubleclick.fastclick;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
public class FastClickMethodVisitor extends AdviceAdapter {
// FastClickHelper 类的内部名称**`**`**************修改这里⬇️
private static final String HELPER_CLASS = "com/csii/baseutil/FastClickHelper";
private final int viewParamIndex;
protected FastClickMethodVisitor(MethodVisitor methodVisitor, int access,
String name, String descriptor) {
super(Opcodes.ASM9, methodVisitor, access, name, descriptor);
this.viewParamIndex = findFirstViewParameterIndex(descriptor);
}
private int findFirstViewParameterIndex(String descriptor) {
Type[] args = Type.getArgumentTypes(descriptor);
for (int i = 0; i < args.length; i++) {
if (args[i].getClassName().equals("android.view.View")) {
return i;
}
}
return 0; // fallback
}
@Override
protected void onMethodEnter() {
// 在方法开头插入:
// if (FastClickHelper.isFastClick(view)) return;
// 加载方法中 为view的下标
loadArg(viewParamIndex);
// 调用静态方法 FastClickHelper.isFastClick(View)
visitMethodInsn(INVOKESTATIC, HELPER_CLASS, "isFastClick", "(Landroid/view/View;)Z", false);
// 判断返回值
org.objectweb.asm.Label label = new org.objectweb.asm.Label();
// 如果 isFastClick == false,
ifZCmp(EQ, label);
// 否则直接返回(拦截点击)
returnValue();
// 正常逻辑的标签
visitLabel(label);
}
public void returnValue() {
visitInsn(Opcodes.RETURN);
}
}
必须 includeBuild
最后。在你的。settings.gradle 中
includeBuild(‘doubleClick’) doubleClick 是你的lib名称
ok 你的项目已经实现了全局的防抖
FastClickHelper.setDisabled(view, true) //这是禁止某个view插桩的调用方法
如果不想下载资源,一下是lib的源码 ⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️
build.gradle
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
}
}
plugins {
id 'java-gradle-plugin'
}
repositories {
google()
mavenCentral()
}
dependencies {
// 注意:使用单引号,没有括号!
compileOnly 'com.android.tools.build:gradle:7.4.2'
implementation 'org.ow2.asm:asm:9.6'
implementation 'org.ow2.asm:asm-commons:9.6'
}
gradlePlugin {
plugins {
create('doubleClick') {
id = 'com.csii.doubleclick.fastclick'
implementationClass = 'com.csii.doubleclick.fastclick.FastClickPlugin'
}
}
}
java {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
com.csii.doubleclick.fastclick
FastClickClassVisitor.JAVA
package com.csii.doubleclick.fastclick;
import com.android.build.api.instrumentation.ClassData;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
/***
* 可以根据
* 自己的需求
* 来进行插桩
*/
public class FastClickClassVisitor extends ClassVisitor {
private String className;
public FastClickClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM9, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
this.className = name;
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
// 匹配目标方法名,且参数包含 View、返回 void
if (("onClick".equals(name) || "onItemClick".equals(name) || name.contains("lambda$"))
&& descriptor.contains("Landroid/view/View;")
&& descriptor.endsWith(")V")) {
System.out.println("[FastClick] Instrumenting: " + className + "." + name + descriptor);
return new FastClickMethodVisitor(mv, access, name, descriptor);
}
return mv;
}
}
FastClickClassVisitorFactory.java
package com.csii.doubleclick.fastclick;
import com.android.build.api.instrumentation.AsmClassVisitorFactory;
import com.android.build.api.instrumentation.ClassContext;
import com.android.build.api.instrumentation.ClassData;
import com.android.build.api.instrumentation.InstrumentationParameters;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Input;
import org.objectweb.asm.ClassVisitor;
public abstract class FastClickClassVisitorFactory
implements AsmClassVisitorFactory<FastClickClassVisitorFactory.Parameters> {
public interface Parameters extends InstrumentationParameters {
@Input
Property<Boolean> getEnabled();
}
@Override
public ClassVisitor createClassVisitor(ClassContext classContext, ClassVisitor nextClassVisitor) {
if (getParameters().get().getEnabled().get()) {
return new FastClickClassVisitor(nextClassVisitor);
}
return nextClassVisitor;
}
@Override
public boolean isInstrumentable(ClassData classData) {
String className = classData.getClassName();
//这里根据自己的包名,过滤,我是只做我项目的过滤
if (className.startsWith("com.***.***") ||
className.startsWith("com.***.***")) {
return true;
}
// 过滤掉系统类和不需要插桩的类
// if (className.startsWith("android.")) return false;
// if (className.startsWith("androidx.")) return false;
// if (className.startsWith("kotlin.")) return false;
// if (className.contains("R$")) return false;
// if (className.endsWith("R")) return false;
// if (className.endsWith("BuildConfig")) return false;
// if (className.contains("FastClickHelper")) return false;
return false;
}
}
FastClickMethodVisitor。java
package com.csii.doubleclick.fastclick;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
public class FastClickMethodVisitor extends AdviceAdapter {
// FastClickHelper 类的内部名称
private static final String HELPER_CLASS = "com/csii/baseutil/FastClickHelper";
private final int viewParamIndex;
protected FastClickMethodVisitor(MethodVisitor methodVisitor, int access,
String name, String descriptor) {
super(Opcodes.ASM9, methodVisitor, access, name, descriptor);
this.viewParamIndex = findFirstViewParameterIndex(descriptor);
}
private int findFirstViewParameterIndex(String descriptor) {
Type[] args = Type.getArgumentTypes(descriptor);
for (int i = 0; i < args.length; i++) {
if (args[i].getClassName().equals("android.view.View")) {
return i;
}
}
return 0; // fallback
}
@Override
protected void onMethodEnter() {
// 在方法开头插入:
// if (FastClickHelper.isFastClick(view)) return;
// 加载第一个参数(View view)
loadArg(viewParamIndex);
// 调用静态方法 FastClickHelper.isFastClick(View)
visitMethodInsn(INVOKESTATIC, HELPER_CLASS, "isFastClick", "(Landroid/view/View;)Z", false);
// 判断返回值
org.objectweb.asm.Label label = new org.objectweb.asm.Label();
// 如果 isFastClick == false,跳转到正常逻辑
ifZCmp(EQ, label);
// 否则直接返回(拦截点击)
returnValue();
// 正常逻辑的标签
visitLabel(label);
}
public void returnValue() {
visitInsn(Opcodes.RETURN);
}
}
FastClickPlugin。java 插件类
package com.csii.doubleclick.fastclick;
import com.android.build.api.instrumentation.FramesComputationMode;
import com.android.build.api.instrumentation.InstrumentationParameters;
import com.android.build.api.instrumentation.InstrumentationScope;
import com.android.build.api.variant.AndroidComponentsExtension;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import kotlin.Unit;
public class FastClickPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
AndroidComponentsExtension<?, ?, ?> androidComponents =
project.getExtensions().findByType(AndroidComponentsExtension.class);
if (androidComponents == null) {
System.err.println("FastClickPlugin: AndroidComponentsExtension not found");
return;
}
// 为所有变体注册插桩
androidComponents.onVariants(
androidComponents.selector().all(),
variant -> {
System.out.println("FastClickPlugin: instrumenting " + variant.getName());
variant.getInstrumentation().transformClassesWith(
FastClickClassVisitorFactory.class,
InstrumentationScope.ALL,
params -> {
params.getEnabled().set(true);
return Unit.INSTANCE;
}
);
variant.getInstrumentation().setAsmFramesComputationMode(
FramesComputationMode.COMPUTE_FRAMES_FOR_INSTRUMENTED_METHODS
);
}
);
}
}

1241

被折叠的 条评论
为什么被折叠?



