萬萬沒想到,做防重複點擊都會被坑4下

  • 2020 年 2 月 23 日
  • 筆記

前幾天,發現App設置頁中有一堆的入口,點擊一些item快點會啟動兩個頁,舉個例子,就比如說微信這個發現頁:

這裡,點擊每個入口都會進入一個新的Activity,但是,如果快速點擊的話,比如快速點擊附近的人,將會出現兩個附近的人頁。

因此,我們要如何解決這個問題呢?

  1. 將所有的Activity設置為singleTop
  2. 對附近的人這個按鈕的onClick事件做一個防止重複點擊

兩種方式都是沒問題的,但是,卻都有問題,首頁我們來分析第一種:

將所有的Activity設置為singleTop

為什麼說這種方式有問題,首先,我們要了解singleTop啟動模式是幹嘛,他是說,如果當前Activity已經在棧頂了,那麼,就不在啟動一個新的這個Activity,只是調用它的onNewIntent,我們能排除一定不會在棧頂已經有這個Activity的時候,在開同樣的頁面嗎?不能!業務千變萬化。

那麼,singleTask可以嗎?抱歉更加不行,singleTask表示如果這個頁面棧中有這個Activity的話,就復用它,並且幹掉處在它上面的所有Activity,讓自己處於棧頂,妥妥的踢人上位,誰,因此,我們更不可能將所有的Activity設置為singleTask模式了。

所以,第一種方式弊端很明顯:

  • 我們不能為了方式用會多開頁面,就以偏概全,將所有的Activity設置為singleTop。
  • 而且,這麼做局限性很大,因為沒有看到問題的本質,問題的本質是因為onClick執行兩次造成的,而出現兩個Activity只是結果。
  • 我們卻要對這個結果進行容錯,而不是針對引發這個現象的源頭進行處理,就有點本末倒置了。

針對這個按鈕的onClick事件做一個防止重複點擊

嗯,這回看似已經找到了問題造成的根源了,如是,你這麼寫:

    btNeayby.setOnClickListener(new View.OnClickListener() {          @Override          public void onClick(View v) {              long nowTime = System.currentTimeMillis();              if (nowTime - mLastClickTime > TIME_INTERVAL) {                  enterActiviy()              }          }      });  }

一些變數就不在這裡給出了,相信你也能看懂這個邏輯,對一處點擊能起到防止重複點擊的效果,那麼,其他地方呢?其他地方你都要寫這樣一段邏輯,都要定義一個最後一次點擊的時間,好麻煩~~

所以,有沒有辦法,不用去定義這些變數,去寫包裹邏輯,回答是有的

RxView.clicks(view)      .throttleFirst(1, TimeUnit.SECONDS)      .subscribe(new Consumer<Object>() {          @Override          public void accept(Object o) throws Exception {              enterActiviy()          }       });

嗯,看起來貌似是可以了,比第一版簡潔不少,沒有mLastClickTime變數的定義了,但是,項目中肯定有很多地方需要點擊事件的,難不成,你每個地方都用RxView.clicks去包裹一遍 所以,有沒有再簡潔一點的呢,答案是有的

Android APT(編譯時程式碼生成),相信對這個有所了解的小夥伴大概知道我會說什麼了?如果你還不了解這個灰科技,可以看看這篇文章 Android APT(編譯時程式碼生成)最佳實踐

解決思路我幫你理一理:

  1. 定義一個註解Annotation,比如就叫做SingleClick
  2. 有了APT這個灰科技,在編譯時根據這個Annotation生成了相關的程式碼。

相信了解過ButterKnife的同學應該知道:

 @OnClick(R.id.bt_submit)      public void submit() {          title.setText("hello world");      }

這個註解,實際上他做了什麼事呢?

  1. 生成程式碼將R.id.bt_submit 通過findViewBy()綁定到一個變數,比如mSubmit上來。
  2. mSubmit設置onClick事件。
  3. 在onClick事件的處理中,將處理權轉發給submit這個被onClick註解方法處理而已
  @Override      public void onClick(View v) {          Method method = null;          try {              method = receiver.getClass().getMethod(clickMethodName);              if (method != null) {                  method.invoke(receiver);              }          } catch (Exception e) {              e.printStackTrace();              Log.e(TAG, "未找到:" + clickMethodName + "方法");          }              try {                  if (method == null) {                      method = receiver.getClass().getMethod(clickMethodName, View.class);                      if (method != null) {                          method.invoke(receiver, v);                      }                  }              } catch (Exception e) {                  e.printStackTrace();                  Log.e(TAG, "未找到帶view類型參數的:" + clickMethodName + "方法");              }      }

只是,ButterKnife的OnClick註解並沒有做防重複點擊。

ButterKnife他沒做防重複的事情你可不可以加,當然是可以的

加了之後,是不是可以寫成這樣子了?

 @SingleClick(R.id.bt_submit)   public void submit() {          title.setText("hello world");   }

然後submit是被被轉發過來,就看你APT的邏輯了。

眼看都到了這個份上了,就這樣玩了嗎?當然還沒有,我們:

我們還不滿足,因為,加入老子就是不喜歡用APT框架怎麼辦?就是不喜歡自己寫view.setOnlickListenser(...)

我們最終祭出終極大殺器,AOP

可能,知道點AOP的同學就秒懂了,沒錯,面向切面編程,我們為什麼不攔截onClick做點文章呢?

在想到這個方案之後,我就搜索了一下github,果然不出所料,有小夥伴就用這種方式處理了,GitHub – jarryleo/SingleClick: Android點擊事件防重庫

不過,我看到了我不大喜歡的地方,既然老子都用AOP了,幹嘛還喲啊在定義一個註解SingleClick呢?我為什麼不直接攔截所有的onClick呢?

如是,我的方案是:

@Aspect  public class OnClickAspect {      @Pointcut("execution(* onClick(..))")      public void onClickPointcut() {      }        @Around("onClickPointcut()")      public void onClick(ProceedingJoinPoint joinPoint) throws Throwable {            // 取出方法的參數 ,想判是不是 onClick(View viw)這種類型的方法          View view = null;          for (Object arg : joinPoint.getArgs()) {              if (arg instanceof View) {                  view = (View) arg;                  break;              }          }          if (view == null) {              joinPoint.proceed();              return;          }            // 取出方法的註解,如果標記可以多次點擊的,就直接走          MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();          Method method = methodSignature.getMethod();          if (method.isAnnotationPresent(MutilClick.class)){              joinPoint.proceed();              return;          }          if (!XClickUtil.isFastDoubleClick(joinPoint.getTarget(),view, 500)) {              // 不是快速點擊,執行原方法              joinPoint.proceed();          }        }    }

 當然,我在做的過程中,也是發現了4個坑:

  1. 有些地方的點擊需要多次點擊怎麼辦?
  2. 如果在onClick事件中做了轉發怎麼處理?
  3. 如果出現super.onClick(v)怎麼處理?
  4. 打release包就出現NPE了怎麼處理?

以上的第一個問題是客觀存在的,比如,我們連續點擊一個按鈕幾次,彈出我們的後門,因此,我加了一個MutilClick的註解,來規避這種情況,這種情況極少,可能一兩處而已。

然而對於

onClick事件中做了轉發

view.setOnClickListener(new View.OnClickListener() {                      @Override                      public void onClick(View v) {                          listener.onClick(v);                      }                  });

哈哈,你妹啊,這不就是活生生的onClick(v)被瞬間就調了兩次,妥妥的重複點擊了,這肯定就造成頁面上這種情況的按鈕無法點了,怎麼處理,別急,我們發現調用主體不同。實際上這種情況等同於:

A.click(view1)    B.click(view1)

因此,可以判斷一下調用的主體是否一致,具體方法下面會給出。

super.onClick(v)

 @Override      public void onClick(View view) {          super.onClick(view);          switch (view.getId()) {

尷尬了吧,這種時候調用的主體都變成了一個,其實就等於

A.click(view1)    A.click(view1)

啥都一樣,不一樣的就是先後各了幾ms而已,等等,人的手速可能幾ms嗎?顯然是不可能的,因此,我們似乎又找到了路子,所以總結起來,我們的防重複點擊工具類可以這麼寫:

package com.tencent.igame.common.utils;    import android.text.TextUtils;  import android.util.Log;  import android.view.View;    public class XClickUtil {      /**       * 最近一次發生事件的target       */      private static String mLastTargetName;      /**       * 最近一次點擊的時間       */      private static long mLastClickTime;      /**       * 最近一次點擊的控制項ID       */      private static int mLastClickViewId;        /**       * 是否是快速點擊       *       * @param v              點擊的控制項       * @param intervalMillis 時間間期(毫秒)       * @return true:是,false:不是       */      public static boolean isFastDoubleClick(Object target, View v, long intervalMillis) {          int viewId = v.getId();          long time = System.currentTimeMillis();          long timeInterval = Math.abs(time - mLastClickTime);          //10,表示手速不可能這麼快,突破ms          if (timeInterval>10 && timeInterval < intervalMillis && viewId == mLastClickViewId && TextUtils.equals(getTargetHash(target), mLastTargetName)) {              Log.e("XClickUtil", "重複點擊 target = [" + getTargetHash(target) + "], v = [" + v.getId() + "], currentTimeMillis = [" + time + "]");              return true;          } else {  //         fixme   這裡其實可以加一下自動埋點              Log.e("XClickUtil", "單次點擊 target = [" + getTargetHash(target) + "], v = [" + v.getId() + "], currentTimeMillis = [" + time + "]");              mLastTargetName = getTargetHash(target);              mLastClickTime = time;              mLastClickViewId = viewId;              return false;          }      }        private static String getTargetHash(Object object) {          return object.getClass().getName() + "@" + object.hashCode();      }  }

最後一個坑,打release包直接就NPE

這種情況肯定就是混淆導致的了,一般加上混淆配置就OK了

#-------------------------註解AOP----------------------  -adaptclassstrings  -keepattributes InnerClasses, EnclosingMethod, Signature, *Annotation*  -keepnames @org.aspectj.lang.annotation.Aspect class * {      ajc* <methods>;  }