Android | 使用 AspectJ 限制按鈕快速點選

augfun發表於2020-11-19

前言

在Android開發中,限制按鈕快速點選(按鈕防抖)是一個常見的需求;
在這篇文章裡,我將介紹一種使用AspectJ的方法,基於註解處理器 & 執行時註解反射的原理。如果能幫上忙,請務必點贊加關注,這真的對我非常重要。

目錄


1. 定義需求

在開始講解之前,我們先 定義需求,具體描述如下:

  • 限制快速點選需求 示意圖:


2. 常規處理方法

目前比較常見的限制快速點選的處理方法有以下兩種,具體如下:

2.1 封裝代理類

封裝一個代理類處理點選事件,代理類通過判斷點選間隔決定是否攔截點選事件,具體程式碼如下:

// 代理類
public abstract class FastClickListener implements View.OnClickListener {
    private long mLastClickTime;
    private long interval = 1000L;

    public FastClickListener() {
    }

    public FastClickListener(long interval) {
        this.interval = interval;
    }

    @Override
    public void onClick(View v) {
        long currentTime = System.currentTimeMillis();
        if (currentTime - mLastClickTime > interval) {
            // 經過了足夠長的時間,允許點選
            onClick();
            mLastClickTime = nowTime;
        } 
    }

    protected abstract void onClick();
}

在需要限制快速點選的地方使用該代理類,具體如下:

tv.setOnClickListener(new FastClickListener() {
    @Override
    protected void onClick() {
        // 處理點選邏輯
    }
});

2.2 RxAndroid 過濾表示式

使用RxJava的過濾表示式throttleFirst也可以限制快速點選,具體如下:

RxView.clicks(view)
    .throttleFirst(1, TimeUnit.SECONDS)
    .subscribe(new Consumer<Object>() {
        @Override
        public void accept(Object o) throws Exception {
            // 處理點選邏輯
        }
     });

2.3 小結

代理類RxAndroid過濾表示式這兩種處理方法都存在兩個缺點:

  • 1. 侵入核心業務邏輯,需要將程式碼替換到需要限制點選的地方;
  • 2. 修改工作量大,每一個增加限制點選的地方都要修改程式碼。

我們需要一種方案能夠規避這兩個缺點 —— AspectJAspectJ是一個流行的Java AOP(aspect-oriented programming)程式設計擴充套件框架,若還不瞭解,請務必檢視文章:《Android | 一文帶你全面瞭解 AspectJ 框架》


3. 詳細步驟

在下面的內容裡,我們將使用AspectJ框架,把限制快速點選的邏輯作為核心關注點從業務邏輯中抽離出來,單獨維護。具體步驟如下:

步驟1:新增AspectJ依賴

  • 1.依賴滬江的AspectJXGradle外掛 —— 在專案build.gradle中新增外掛依賴:
// 專案級build.gradle
dependencies {
    classpath 'com.android.tools.build:gradle:3.5.3'
    classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.8'
}

如果外掛下載速度過慢,可以直接依賴外掛 jar檔案,將外掛下載到專案根目錄(如/plugins),然後在專案build.gradle中新增外掛依賴:

// 專案級build.gradle
dependencies {
    classpath 'com.android.tools.build:gradle:3.5.3'
    classpath fileTree(dir:'plugins', include:['*.jar'])
}
  • 2.應用外掛 —— 在App Modulebuild.gradle中應用外掛:
// App Module的build.gradle
apply plugin: 'android-aspectjx'
...
  • 3.依賴AspectJ框架 —— 在包含AspectJ程式碼的Modulebuild.gradle檔案中新增依賴:
// Module級build.gradle
dependencies {
    ...
    api 'org.aspectj:aspectjrt:1.8.9'
    ...
}

步驟2:實現判斷快速點選的工具類

  • 我們先實現一個判斷View是否快速點選的工具類;
  • 實現原理是使用Viewtag屬性儲存最近一次的點選時間,每次點選時判斷當前時間距離儲存的時間是否已經經過了足夠長的時間;
  • 為了避免呼叫View#setTag(int key,Object tag)時傳入的key與其他地方傳入的key衝突而造成覆蓋,務必使用在資原始檔中定義的 id,資原始檔中的 id 能夠有效保證全域性唯一性,具體如下:
// ids.xml
<resources>
    <item type="id" name="view_click_time" />
</resources>

 

public class FastClickCheckUtil {

    /**
     * 判斷是否屬於快速點選
     *
     * @param view     點選的View
     * @param interval 快速點選的閾值
     * @return true:快速點選
     */
    public static boolean isFastClick(@NonNull View view, long interval) {
        int key = R.id.view_click_time;

        // 最近的點選時間
        long currentClickTime = System.currentTimeMillis();

        if(null == view.getTag(key)){
            // 1\. 第一次點選

            // 儲存最近點選時間
            view.setTag(key, currentClickTime);
            return false;
        }
        // 2\. 非第一次點選

        // 上次點選時間
        long lastClickTime = (long) view.getTag(key);
        if(currentClickTime - lastClickTime < interval){
            // 未超過時間間隔,視為快速點選
            return true;
        }else{
            // 儲存最近點選時間
            view.setTag(key, currentClickTime);
            return false;
        }
    }
}

步驟3:定義Aspect切面

使用@Aspect註解定義一個切面,使用該註解修飾的類會被AspectJ編譯器識別為切面類:

@Aspect
public class FastClickCheckerAspect {
    // 隨後填充
}

步驟4:定義PointCut切入點

使用@Pointcut註解定義一個切入點,編譯期AspectJ編譯器將搜尋所有匹配的JoinPoint,執行織入:

@Aspect
public class FastClickAspect {

    // 定義一個切入點:View.OnClickListener#onClick()方法
    @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void methodViewOnClick() {
    }

    // 隨後填充 Advice
}

步驟5:定義Advice增強

增強的方式有很多種,在這裡我們使用@Around註解定義環繞增強,它將包裝PointCut,在PointCut前後增加橫切邏輯,具體如下:

@Aspect
public class FastClickAspect {

    // 定義切入點:View.OnClickListener#onClick()方法
    @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void methodViewOnClick() {}

    // 定義環繞增強,包裝methodViewOnClick()切入點
    @Around("methodViewOnClick()")
    public void aroundViewOnClick(ProceedingJoinPoint joinPoint) throws Throwable {
        // 取出目標物件
        View target = (View) joinPoint.getArgs()[0];
        // 根據點選間隔是否超過2000,判斷是否為快速點選
        if (!FastClickCheckUtil.isFastClick(target, 2000)) {
            joinPoint.proceed();
        }
    }
}

步驟6:實現View.OnClickListener

在這一步我們為View設定OnClickListener,可以看到我們並沒有新增限制快速點選的相關程式碼,增強的邏輯對原有邏輯沒有侵入,具體程式碼如下:

// 原始碼:
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.text).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i("AspectJ","click");
            }
        });
    }
}

編譯程式碼,隨後反編譯AspectJ編譯器執行織入後的.class檔案。還不瞭解如何查詢編譯後的.class檔案,請務必檢視文章:《Android | 一文帶你全面瞭解 AspectJ 框架》

public class MainActivity extends AppCompatActivity {
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(2131361820);
    findViewById(2131165349).setOnClickListener(new View.OnClickListener() {
          private static final JoinPoint.StaticPart ajc$tjp_0;

          // View.OnClickListener#onClick()
          public void onClick(View v) {
            View view = v;
            // 重構JoinPoint,執行環繞增強,也執行@Around修飾的方法
            JoinPoint joinPoint = Factory.makeJP(ajc$tjp_0, this, this, view);
            onClick_aroundBody1$advice(this, view, joinPoint, FastClickAspect.aspectOf(), (ProceedingJoinPoint)joinPoint);
          }

          static {
            ajc$preClinit();
          }

          private static void ajc$preClinit() {
            Factory factory = new Factory("MainActivity.java", null.class);
            ajc$tjp_0 = factory.makeSJP("method-execution", (Signature)factory.makeMethodSig("1", "onClick", "com.have.a.good.time.aspectj.MainActivity$1", "android.view.View", "v", "", "void"), 25);
          }

          // 原來在View.OnClickListener#onClick()中的程式碼,相當於核心業務邏輯
          private static final void onClick_aroundBody0(null ajc$this, View v, JoinPoint param1JoinPoint) {
            Log.i("AspectJ", "click");
          }

          // @Around方法中的程式碼,即原始碼中的aroundViewOnClick(),相當於Advice
          private static final void onClick_aroundBody1$advice(null ajc$this, View v, JoinPoint thisJoinPoint, FastClickAspect ajc$aspectInstance, ProceedingJoinPoint joinPoint) {
            View target = (View)joinPoint.getArgs()[0];
            if (!FastClickCheckUtil.isFastClick(target, 2000)) {
              // 非快速點選,執行點選邏輯
              ProceedingJoinPoint proceedingJoinPoint = joinPoint;
              onClick_aroundBody0(ajc$this, v, (JoinPoint)proceedingJoinPoint);
              null;
            } 
          }
        });
  }
}

小結

到這裡,我們就講解完使用AspectJ框架限制按鈕快速點選的詳細,總結如下:

  • 使用@Aspect註解描述一個切面,使用該註解修飾的類會被AspectJ編譯器識別為切面類;
  • 使用@Pointcut註解定義一個切入點,編譯期AspectJ編譯器將搜尋所有匹配的JoinPoint,執行織入;
  • 使用@Around註解定義一個增強,增強會被織入匹配的JoinPoint

4. 演進

現在,我們迴歸文章開頭定義的需求,總共有4點。其中前兩點使用目前的方案中已經能夠實現,現在我們關注後面兩點,即允許定製時間間隔覆蓋儘可能多的點選場景

  • 需求迴歸 示意圖:

4.1 定製時間間隔

在實際專案不同場景中的按鈕,往往需要限制不同的點選時間間隔,因此我們需要有一種簡便的方式用於定製不同場景的時間間隔,或者對於一些不需要限制快速點選的地方,有辦法跳過快速點選判斷,具體方法如下:

  • 定義註解
/**
 * 在需要定製時間間隔地方新增@FastClick註解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface FastClick {
    long interval() default FastClickAspect.FAST_CLICK_INTERVAL_GLOBAL;
}
  • 修改切面類的Advice
@Aspect
public class SingleClickAspect {

    public static final long FAST_CLICK_INTERVAL_GLOBAL = 1000L;

    @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void methodViewOnClick() {}

    @Around("methodViewOnClick()")
    public void aroundViewOnClick(ProceedingJoinPoint joinPoint) throws Throwable {
        // 取出JoinPoint的簽名
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 取出JoinPoint的方法
        Method method = methodSignature.getMethod();

        // 1\. 全域性統一的時間間隔
        long interval = FAST_CLICK_INTERVAL_GLOBAL;

        if (method.isAnnotationPresent(FastClick.class)) {
            // 2\. 如果方法使用了@FastClick修飾,取出定製的時間間隔

            FastClick singleClick = method.getAnnotation(FastClick.class);
            interval = singleClick.interval();
        }
        // 取出目標物件
        View target = (View) joinPoint.getArgs()[0];
        // 3\. 根據點選間隔是否超過interval,判斷是否為快速點選
        if (!FastClickCheckUtil.isFastClick(target, interval)) {
            joinPoint.proceed();
        }
    }
}
  • 使用註解
findViewById(R.id.text).setOnClickListener(new View.OnClickListener() {
    @FastClick(interval = 5000L)
    @Override
    public void onClick(View v) {
        Log.i("AspectJ","click");
    }
});

4.2 完整場景覆蓋

ButterKnife @OnClick android:onClick OK RecyclerView / ListView Java Lambda NO Kotlin Lambda OK DataBinding OK

Editting...



作者:Android進階架構
連結:https://www.jianshu.com/p/597894717331
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。

相關文章