最近項目進入緊鑼密鼓測試階段,昨天測試提了一個issue,app中按鈕都沒有做快速點擊校驗。
這就涉及到aop面向切面編程了!后端開發Spring對aop應該很熟悉,android開發中可能用到aop的情況沒有后端那么多,但是aop對android開發也是至關重要的!
哪些情況用到aop?
- 比如針對某一功能進行埋點
- 全局日志處理
- 全局異常處理
- 全局動畫處理等
java aop大致有三種方式
1.jdk動態代理
2.cglib動態代理
3.aspectj
. | jdk動態代理 | cglib | aspectj |
---|---|---|---|
作用對象的限制 | 只能操作實現了接口的類 | 不能操作被final修飾的類,因為cglib是針對類實現代理,主要是對指定的類生成一個子類,覆蓋其中的方法來實現代理. | 貌似沒什么限制 |
基本原理 | 利用攔截器(攔截器必須實現InvocationHanlder)加上反射機制生成一個實現代理接口的匿名類,在調用具體方法前調用InvokeHandler來處理。 | 利用ASM開源包,對代理對象類的class文件加載進來,通過修改其字節碼生成子類來處理。 | 采用基于jvm的ajc(編譯器)和weaver(織入器),在class字節碼中織入aspectj的代碼 |
項目中我使用的是https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx 它是在aspectj基礎上做了些修改,支持AS的instant run,集成比aspectj更加方便
首先在project下的gradle文件中加入aspectjx插件
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
maven { url 'https://maven.aliyun.com/repository/jcenter/' }
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
google()
jcenter()
maven { url 'https://jitpack.io' }
maven { url 'https://dl.bintray.com/umsdk/release' }
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.1'
classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-beta02"
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
maven { url 'https://maven.aliyun.com/repository/jcenter/' }
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
google()
jcenter()
maven { url 'https://jitpack.io' }
maven { url 'https://dl.bintray.com/umsdk/release' }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
ext {
compileSdkVersion = 28
minSdkVersion = 17
targetSdkVersion = 27
versionCode=5
versionName="1.4.6"
testRunner="1.1.1"
espresso="3.1.1"
junit="4.12"
appcompat="1.1.0-alpha01"
supportLibVersion = "28.0.0"
}
module下的gradle文件添加
apply plugin: 'android-aspectjx'
接下來就可以開始編寫被@AspectJ 修飾的切面類了
AspectHandler.java 用來對點擊事件相關的攔截處理
package com.mjt.pad.common.aspect;
import android.util.Log;
import com.mjt.common.utils.UIUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
/**
* Copyright:mjt_pad_android
* Author: liyang <br>
* Date:2019-05-05 15:41<br>
* Desc: <br>
*/
@Aspect
public class AspectHandler {
private static final String TAG = AspectHandler.class.getSimpleName();
@Before("execution(void android.view.View.OnClickListener.onClick(..))")
public void beforePoint(JoinPoint joinPoint) {
Log.e(TAG, "before: " + joinPoint);
}
@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
public void dealWithNormal() {
}
@Around("dealWithNormal()")
public void onViewClicked(ProceedingJoinPoint proceedingJoinPoint) {
boolean isFastClickPassed = !UIUtils.isFastClickOnlyInAspect();
Log.e(TAG, "onViewClicked: 捕獲到了,isFastClick=" + !isFastClickPassed);
if (isFastClickPassed) {
Log.e(TAG, "onViewClicked: " + proceedingJoinPoint);
try {
proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
Log.e(TAG, "onViewClicked: ", throwable);
}
}
}
}
簡單介紹下這個類里面的@Before @Pointcut @Around幾個注解吧,不然完全沒接觸過aspectj的同學會看的一頭霧水
首先說pointcut
@Pointcut相當于你要攔截的某些執行點或者調用點,pointcut可以有call,execution,target,this,within,withincode等等操作符,這些操作符可以結合java的||,&&,!使用
call捕獲的joinpoint是簽名方法的調用點,而execution捕獲的則是執行點。
call和execution的語法
within()的參數是一個類,比如我們可以通過within(A.class)或者!within(A.class)來過濾想要攔截的點
withincode()和within()相似,只不過withincode()接收的參數是方法的signature
target()判斷目標對象是否是某種類型,this()判斷當前執行對象是否是某種類型
call和execution的語法結構:
execution/call([注解] [修飾符] 返回值類型 [類型聲明]方法名(參數列表)[ 異常列表]),被[]括住的是非必須項.
舉例:
execution (* com.mjt..*.*(..))
1、execution(): 表達式主體。
2、第一個*號:表示返回類型,*號表示所有的類型。
3、包名:表示需要攔截的包名,后面的兩個句點表示當前包和當前包的所有子包,com.mjtl包、子孫包下所有類的方法。
4、第二個*號:表示類名,*號表示所有的類。
5、*(..):最后這個星號表示方法名,*號表示所有的方法,后面括弧里面表示方法的參數,兩個句點表示任何參數。
@Before在攔截點或者調用點之前調用
@After是在攔截點或者調用點之后調用
被@Around注解的方法,會被織入到攔截方法調用點或這行點之前,
接著我運行項目,編譯通過后,快速的點擊了一個按鈕
log日志顯示
2019-05-08 11:43:41.717 14103-14103/com.mjt.pad.test E/AspectHandler: before: execution(void com.mjt.pad.ui.adapter.ProductAdapter.1.onClick(View))
2019-05-08 11:43:41.718 14103-14103/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=false
2019-05-08 11:43:41.718 14103-14103/com.mjt.pad.test E/AspectHandler: onViewClicked: execution(void com.mjt.pad.ui.adapter.ProductAdapter.1.onClick(View))
2019-05-08 11:43:41.883 14103-14103/com.mjt.pad.test E/AspectHandler: before: execution(void com.mjt.pad.ui.adapter.ProductAdapter.1.onClick(View))
2019-05-08 11:43:41.883 14103-14103/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=true
執行了,before方法,然后進入被@Around修飾的方法,看到第一次點擊判斷不是快速點擊
放過攔截,proceedingJoinPoint.proceed();原方法得到執行!
第二次點擊,判斷是快速點擊,proceedingJoinPoint.proceed()沒有執行,也就是原方法被攔截掉了!
但是這時候我發現如果點擊事件是使用lambda表達式是無法攔截的,因為這里的pointcut的execution是這樣
@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
public void dealWithNormal() {
}
這個正則的大致意思是,攔截返回類型為void, android.view.View.OnClickListener.onClick()方法,參數(..)表示參數可以是任意數量任意類型
那么接著寫pointcut攔截lambda表達式的點擊事件
于是AspectHandler 切面類被我改成了這樣
package com.mjt.pad.common.aspect;
import android.util.Log;
import com.mjt.common.utils.UIUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
/**
* Copyright:mjt_pad_android
* Author: liyang <br>
* Date:2019-05-05 15:41<br>
* Desc: <br>
*/
@Aspect
public class AspectHandler {
private static final String TAG = AspectHandler.class.getSimpleName();
@Before("dealWithNormal()||dealWithLambda()")
public void beforePoint(JoinPoint joinPoint) {
Log.e(TAG, "before: " + joinPoint);
}
@Pointcut("execution(void com.mjt..lambda*(android.view.View))")
public void dealWithLambda() {
}
@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
public void dealWithNormal() {
}
@Around("dealWithNormal()||dealWithLambda()")
public void onViewClicked(ProceedingJoinPoint proceedingJoinPoint) {
boolean isFastClickPassed = !UIUtils.isFastClickOnlyInAspect();
Log.e(TAG, "onViewClicked: 捕獲到了,isFastClick=" + !isFastClickPassed);
if (isFastClickPassed) {
Log.e(TAG, "onViewClicked: " + proceedingJoinPoint);
try {
proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
Log.e(TAG, "onViewClicked: ", throwable);
}
}
}
}
增加的pointcut對點擊事件采用lambda表達式的攔截
然后我快速的點擊了一個采用lambda表達式方式實現的點擊事件log日志如下
2019-05-08 11:55:05.506 15052-15052/com.mjt.pad.test E/AspectHandler: before: execution(void com.mjt.pad.ui.fragment.print.PrintManagerFragment.lambda$initViews$1(View))
2019-05-08 11:55:05.506 15052-15052/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=false
2019-05-08 11:55:05.506 15052-15052/com.mjt.pad.test E/AspectHandler: onViewClicked: execution(void com.mjt.pad.ui.fragment.print.PrintManagerFragment.lambda$initViews$1(View))
2019-05-08 11:55:05.755 15052-15052/com.mjt.pad.test E/AspectHandler: before: execution(void com.mjt.pad.ui.fragment.print.PrintManagerFragment.lambda$initViews$1(View))
2019-05-08 11:55:05.755 15052-15052/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=true
嗯,lambda方式實現的點擊事件也被攔截到了
接下來,如果某個小伙伴或我這個按鈕不要攔截快速點擊,那怎么辦呢?
嗯,采用自定義注解,如果某個onClick方法請求放過快速點擊攔截,加上這個注解就好了
接著我們寫一個自定義注解就叫Ignore
作用于CLASS,修飾的目標為方法和構造函數
package com.mjt.pad.common.aspect;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Copyright:mjt_pad_android
* Author: liyang <br>
* Date:2019-05-05 16:35<br>
* Desc: <br>
*/
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD,ElementType.CONSTRUCTOR})
public @interface Ignore {
}
接著改動AspectHandler切面類
package com.mjt.pad.common.aspect;
import android.util.Log;
import com.mjt.common.utils.UIUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
/**
* Copyright:mjt_pad_android
* Author: liyang <br>
* Date:2019-05-05 15:41<br>
* Desc: <br>
*/
@Aspect
public class AspectHandler {
private static final String TAG = AspectHandler.class.getSimpleName();
private volatile boolean isIgnored = false;
@Before("execution(@com.mjt.pad.common.aspect.Ignore void com.mjt..*.onClick(..))")
public void checkIgnore(JoinPoint joinPoint) {
isIgnored = true;
Log.e(TAG, "checkIgnore: " + joinPoint);
}
@Pointcut("execution(void com.mjt..lambda*(android.view.View))")
public void dealWithLambda() {
}
@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
public void dealWithNormal() {
}
@Around("dealWithNormal()||dealWithLambda()")
public void onViewClicked(ProceedingJoinPoint proceedingJoinPoint) {
boolean isFastClickPassed = !UIUtils.isFastClickOnlyInAspect();
Log.e(TAG, "onViewClicked: 捕獲到了,isFastClick=" + !isFastClickPassed+",isIgnored="+isIgnored);
if (isIgnored||isFastClickPassed) {
Log.e(TAG, "onViewClicked: " + proceedingJoinPoint);
try {
proceedingJoinPoint.proceed();
isIgnored=false;
} catch (Throwable throwable) {
throwable.printStackTrace();
Log.e(TAG, "onViewClicked: ", throwable);
}
}
}
}
對@Before方法進行了修改
@Before("execution(@com.mjt.pad.common.aspect.Ignore void com.mjt..*.onClick(..))")
public void checkIgnore(JoinPoint joinPoint) {
isIgnored = true;
Log.e(TAG, "checkIgnore: " + joinPoint);
}
這"execution(@com.mjt.pad.common.aspect.Ignore void com.mjt..*.onClick(..))"
匹配的是被我們自定義注解@Ignore修飾的 com.mjt包及其子包下的所有onClick方法
然后我們找一個onClick方法加上@Ignore注解看看起作用沒
@Ignore
@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.llyPart:
...
接著找到這個被Ignore修飾的點擊事件,快速點擊兩下
log日志如下
2019-05-08 12:19:06.925 16912-16912/com.mjt.pad.test E/AspectHandler: checkIgnore: execution(void com.mjt.pad.ui.fragment.RemarkFragment.onClick(View))
2019-05-08 12:19:06.925 16912-16912/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=false,isIgnored=true
2019-05-08 12:19:06.926 16912-16912/com.mjt.pad.test E/AspectHandler: onViewClicked: execution(void com.mjt.pad.ui.fragment.RemarkFragment.onClick(View))
2019-05-08 12:19:07.086 16912-16912/com.mjt.pad.test E/AspectHandler: checkIgnore: execution(void com.mjt.pad.ui.fragment.RemarkFragment.onClick(View))
2019-05-08 12:19:07.087 16912-16912/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=true,isIgnored=true
2019-05-08 12:19:07.087 16912-16912/com.mjt.pad.test E/AspectHandler: onViewClicked: execution(void com.mjt.pad.ui.fragment.RemarkFragment.onClick(View))
可以看到,是快速點擊,但是原方法也得到了執行
嗯 對項目的全局處理點擊事件大致就是這樣,aspectj相關的東西有很多