Android使用AOP來解決重複點選問題
AOP即Aspect Oriented Programming的縮寫,習慣稱為切面程式設計;與OOP(面向物件程式設計)萬物模組化的思想不同,AOP則是將涉及到眾多模組的某一類問題進行統一管理,AOP的優點是將業務邏輯與系統化功能高度解耦,讓我們在開發過程中可以只專注於業務邏輯,其他一些系統化功能(如路由、日誌、許可權控制、攔截器、埋點、事件防抖等)則由AOP統一處理;
AspectJ簡介
AOP是一種程式設計思想,或者說方法論,AspectJ則是專為AOP設計的一種語言,它支援原生的JAVA,可用於在java中處理AOP的相關問題。
下面非常簡單的描述下AspectJ中幾個要點
-
@Aspect
表示這是一個切面類,放在類名上面,把當前類標識為一個切面供容器讀取
@Aspect public class SingleClickAspect { // 裡面用AspectJ註解,實現相應方法 }
-
Join Points
AspectJ中的切點,是AspectJ作用到具體某個位置的說明,主要包括三類:
1、函式(函式呼叫,函式執行,建構函式等)
2、變數(變數get,變數set等)
3、 程式碼塊(靜態程式碼塊,for等)
-
@Pointcuts
AspectJ中的切面(這種翻譯不一定正確),由點及面,用於說明你需要hook哪一類問題,比如我需要hook一個單擊事件SingleClick ,
@Retention(RetentionPolicy.RUNTIME) //註解保留至執行時 @Target(ElementType.METHOD) //宣告註解作用在方法上面 public @interface SingleClick { /* 點選間隔時間 */ long value() default 2000; }
則:
@Aspect public class SingleClickAspect { /** * 定義切點,標記切點為所有被@SingleClick註解的方法 * 注意:這裡com.util.click.SingleClick是你自己專案中SingleClick這個類的全路徑 * 注意:這裡的 * * 表示任意方法 * (..)表示任意引數 */ @Pointcut("execution(@com.util.click.SingleClick * *(..))") public void methodClick() {}// 該方法不會被執行 }
-
advice
Join Points和Pointcuts用來說明需要hook哪些位置或者流程,advice則用於hook之後指定需要做什麼,在切面類中需要定義切面方法用於響應響應的目標方法,切面方法即為通知方法,通知方法需要用註解標識,AspectJ 支援 5 種類型的通知註解:
@Before: 前置通知, 在方法執行之前執行
@After: 後置通知, 在方法執行之後執行 。
@AfterRunning: 返回通知, 在方法返回結果之後執行
@AfterThrowing: 異常通知, 在方法丟擲異常之後
@Around: 環繞通知, 圍繞著方法執行,around()用的會比較多,因為自由度高,其他的用around()都可以實現
@Aspect public class SingleClickAspect { @Pointcut("execution(@com.util.click.SingleClick * *(..))") public void methodClick() {}// 該方法不會被執行 @Before("methodClick()") public void before(){ System.out.println("before................"); } @After("methodClick()") public void after(){ System.out.println("after................."); } @AfterReturning("methodClick()") public void afterReturning(JoinPoint joinPoint) { System.out.println("afterReturning................."); } @AfterThrowing("methodClick()") public void afterThrowing(JoinPoint joinPoint) { System.out.println("afterThrowing..................."); } @Around("methodClick()") public void around(ProceedingJoinPoint joinPoint) throws Throwable{ System.out.println("around before............"); joinPoint.proceed(); //執行完成目標方法 System.out.println("around after.............."); }
它們是按什麼順序執行的呢?建立點選事件
TextView textView.setOnClickListener(new View.OnClickListener() { @SingleClick(1500)// 新增點選註釋 @Override public void onClick(View v) { if (flag) { System.out.println("throw an exception................"); throw new RuntimeException(); } System.out.println("執行onClick................"); } });
點選後,執行正常情況結果:
around before............ before................ 執行onClick................ around after.............. after................. afterReturning.................
執行異常情況結果:
around before............ before................ throw an exception................ around after.............. after................. afterThrowing.................
對於@Around這個advice,不管它有沒有返回值,但是必須要在方法內部,呼叫一下joinPoint.proceed();否則,OnClickListener中的onClick()將沒有機會被執行,從而也導致了 @Before這個advice不會被觸發。
AOP處理android中的重複點選
AOP用於處理某一類獨立的問題,非常契合遮蔽重複點選的需求,我們只需要hook住原先的點選事件(轉確的說是點選事件後的處理流程),判斷是不是重複點選,是則過濾掉不讓它執行,否則就正常執行;
整合
1.引入Aspectj
在Android中進行AspectJ的實現,建議使用Hujiang大神的框架gradle_plugin_android_aspectjx ,可以非常方便的整合和配置AspectJ在Android中的環境
- 在專案根目錄下的build.gradle中,新增依賴:
dependencies { classpath 'com.android.tools.build:gradle:3.3.1' classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4' }
- 在app或其他任何用到該AOP功能的module目錄下的build.gradle中,都需新增:
apply plugin: 'android-aspectjx' dependencies { ...... implementation 'org.aspectj:aspectjrt:1.8.9' }
2.新增一個自定義註解
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface SingleClick { /* 點選間隔時間 */ long value() default 1000; }
- 新增自定義註解的原因是,方便管理哪些方法使用了重複點選的AOP,同時可以在註解中傳入點選時間間隔,更加靈活。
3.封裝一個重複點選判斷工具類
public final class XClickUtil { /** * 最近一次點選的時間 */ private static long mLastClickTime; /** * 最近一次點選的控制元件ID */ private static int mLastClickViewId; /** * 是否是快速點選 * * @param v點選的控制元件 * @param intervalMillis時間間期(毫秒) * @returntrue:是,false:不是 */ public static boolean isFastDoubleClick(View v, long intervalMillis) { int viewId = v.getId(); //long time = System.currentTimeMillis(); long time = SystemClock.elapsedRealtime(); long timeInterval = Math.abs(time - mLastClickTime); if (timeInterval < intervalMillis && viewId == mLastClickViewId) { Log.e("isFastDoubleClick", "true"); return true; } else { mLastClickTime = time; mLastClickViewId = viewId; Log.e("isFastDoubleClick", "false"); return false; } } }
4.編寫Aspect AOP處理類
@Aspect public class SingleClickAspect { private static final long DEFAULT_TIME_INTERVAL = 5000; /** * 定義切點,標記切點為所有被@SingleClick註解的方法 * 注意:這裡me.baron.test.annotation.SingleClick需要替換成 * 你自己專案中SingleClick這個類的全路徑哦 */ @Pointcut("execution(@me.baron.test.annotation.SingleClick * *(..))") public void methodAnnotated() {} /** * 定義一個切面方法,包裹切點方法 */ @Around("methodAnnotated()") public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable { // 取出方法的引數 View view = null; for (Object arg : joinPoint.getArgs()) { if (arg instanceof View) { view = (View) arg; break; } } if (view == null) { return; } // 取出方法的註解 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); if (!method.isAnnotationPresent(SingleClick.class)) { return; } SingleClick singleClick = method.getAnnotation(SingleClick.class); // 判斷是否快速點選 if (!XClickUtil.isFastDoubleClick(view, singleClick.value())) { // 不是快速點選,執行原方法 joinPoint.proceed(); } } }
使用方法
private void initView() { btTest = findViewById(R.id.bt_test); btTest.setOnClickListener(new View.OnClickListener() { // 如果需要自定義點選時間間隔,自行傳入毫秒值即可 // @SingleClick(2000) @SingleClick @Override public void onClick(View v) { // do something } }); }
遇到的坑
監聽系統的onClick()方法時,有時會多次呼叫切點的方法,導致onClick()方法失效,
@Pointcut("execution(* android.view.View.OnClickListener.onClick(..))") public void methodAnnotated() {}
原因:onClick()方法中又呼叫了onClick()方法,被判定為重複點選,所以點選事件沒有執行,例如:
@Override public void onClick(View v) { super.onClick(v);// 重複呼叫 }
if (posListener != null) { btnPositive.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { dialog.dismiss(); posListener.onClick(view);// 重複呼叫 } });