Android 全局防抖/防重复点击

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
                    );
                }
        );
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值