Android AOP 面向切面編程 - AspectJ

AOP 概念

AOP 是 Aspect Oriented Programming 的縮寫,意爲 面向切面編程,通過預編譯和運行期動態代理實現程序功能的統一維護的一種技術。利用 AOP 可以實現對代碼的業務邏輯進行隔離,降低各功能間的耦合度。

使用場景: 需求是在類的每個方法中代碼執行之前添加一句日誌打印,在沒有使用 AOP 的情況下,就需要在每個方法中手動添加日誌打印,使用了 AOP ,就可以將打印日誌代碼在編譯期間插入方法中,我們維護時二者的邏輯是分開的。

Android AOP 的實現方式

AOP 和 OOP 一樣只是一種編程思想,它的實現方式主要有以下幾種:

  • APT :AnnotationProcessor 在編譯時生成 Java 文件。
  • AspectJ : 在將 .java 文件編譯爲 .class 文件時進行代碼的注入。
  • Javassist : 對編譯好的 class 字節碼文件進行操作。

![未命名文件 (1)](/Users/xing/Downloads/未命名文件 (1).png)

AOP 術語

JoinPoint:連接點,程序執行過程中明確的點(被攔截的方法,字段,構造器)

Pointcut : 切點,用來描述 JoinPoint 連接點的表達式。比如描述調用 Animal.fly() 方法的地方,則寫成

call(* Animal.fly(**))

Advice : 增強,表示在 Pointcut 裏面定義的程序點具體要做的操作,通過 before,after,around 來區分是在 Jointpoint 之前,之後還是代替執行的代碼。

Aspect :切面,類似 Java 中的類聲明,Pointcut 和 Advice 合在一起稱作 Aspect。

JoinPoint 說明 Pointcut 語法
method call 函數被調用 call(MethodSignature)
method execution 函數執行內部 execution(MethodSignature)
constructor call 構造函數被調用 call(MethodSignature)
constructor execution 構造函數執行內部 execution(ConstructorSignature)
field get 讀成員變量 get(FieldSignature)
field set 寫成員變量 set(FieldSignature)
static initialization static 塊初始化 staticinitialization(TypeSignature)
handler 異常處理 handler(TypeSignature)只能與 @Before()配合使用
advice execution advice 執行 adviceexecution
Advice 說明
@Before(Pointcut) 執行 JoinPoint 之前
@After(Pointcut) 執行 JoinPoint 之後
@AfterReturning @AfterReturning(pointcut=-“xx”,returning=“returnValue”)
@AfterThrowing @AfterThrowing(pointcut=“xx”,throwing=“throwable”)
@Around(Pointcut) 替代原來的代碼,如果要執行原來的代碼,需要使用 ProceedingJoinPoint.proceed()

AOP 表達式 & 通配符

execution 基本語法:

execution(<修飾符模式>?<返回類型模式><方法名模式>(<參數模式>)<異常模式>?)

除了返回類型模式,方法名模式,參數模式外,其他項都是可選的。

  • execution(public * *(…)) : 匹配目標類所有 public 方法, 第一個 * 代表返回類型,第二個 * 代表方法名,而 … 代表任意方法參數。
  • execution(* *To(…)) : 匹配目標類所有以 To 結尾的方法,第一個 * 代表任意返回類型,而 *To 代表任意以 To 結尾的方法。
  • execution(* com.xing.MainActivity.*(…)) : 匹配 MainActivity 中所有的方法,第一個 * 代表任意返回類型,第二個 * 代表方法名任意。
  • execution(*com.xing.demo.MainPresenter+.*(…)) : 匹配 MainPresenter 接口及其所有實現類的方法。
  • execution(* com.xing.*(…)) : 匹配 com.xing 包下所有類的所有方法。
  • execution(* com.xing…*(…)) : 匹配 com.xing 包,子孫包下所有類的所有方法。
  • execution(* com…*.*Dao.find*(…)) : 匹配 com 包,子孫包下以 Dao 結尾的類中所有以 find 爲前綴的方法。

Android AspectJ 實現 AOP

Android 中使用 AspectJ 步驟:

(1) 項目根目錄 build.gradle 配置 classpath

buildscript {
    repositories {
        google()
        jcenter()
        mavenCentral()   // AspectJ 需要 maven
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
        classpath 'org.aspectj:aspectjtools:1.8.9'
//        classpath 'org.aspectj:aspectjweaver:1.8.9'
    }
}

(2) app build.gradle 添加依賴和 task

apply plugin: 'com.android.application'
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.xing.aspectjsample"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

final def log = project.logger
final def variants = project.android.applicationVariants

variants.all { variant ->
    if (!variant.buildType.isDebuggable()) {
        log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
        return;
    }

    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        String[] args = ["-showWeaveInfo",
                         "-1.8",
                         "-inpath", javaCompile.destinationDir.toString(),
                         "-aspectpath", javaCompile.classpath.asPath,
                         "-d", javaCompile.destinationDir.toString(),
                         "-classpath", javaCompile.classpath.asPath,
                         "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
        log.debug "ajc args: " + Arrays.toString(args)

        MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler);
        for (IMessage message : handler.getMessages(null, true)) {
            switch (message.getKind()) {
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error message.message, message.thrown
                    break;
                case IMessage.WARNING:
                    log.warn message.message, message.thrown
                    break;
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break;
            }
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'org.aspectj:aspectjrt:1.8.9'
}

AspectJ 實戰

實戰1 - Activity 生命週期函數中添加 log 打印。

/**
 * 定義切面
 */
@Aspect
public class TraceAspect {
    /**
    * 定義切點,攔截 MainActivity 中所有以 on 爲前綴的方法
    */
    @Pointcut("execution(* com.xing.aspectjsample.MainActivity.on*(..))")
    public void onLifecycleLog(){}

    /**
    * 在所有以 on 爲前綴的方法執行前添加 log.e 打印
    */
    @Before("onLifecycleLog()")
    public void handleLifecycleLog(JoinPoint joinPoint){
        String name = joinPoint.getSignature().getName();
        Log.e("MainActivity", name + "-------->>>>>" + joinPoint);
    }
}
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "TraceAspect";

    private int count = 12;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        int count = getCount();
    }

    private int getCount() {
        return count;
    }

    @SingleClick
    public void click(View view) {
        Log.e(TAG, "click: ");
        Toast.makeText(this, "the button is clicked", Toast.LENGTH_SHORT).show();
    }

    @Override
    protected void onStart() {
        super.onStart();
    }


    @Override
    protected void onResume() {
        super.onResume();
    }


    @Override
    protected void onPause() {
        super.onPause();
    }

    @Override
    protected void onStop() {
        super.onStop();
    }

    @Override
    protected void onRestart() {
        super.onRestart();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

在這裏插入圖片描述

實戰2 - 防止按鈕重複點擊

防止按鈕重複點擊思路是:兩次點擊的時間間隔小於指定值,不產生點擊作用。

定義註解 SingleClick

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface SingleClick {
    long value() default 1000;
}

定義切面類,攔截類中標註了 SingleClick 註解,參數爲 View 的方法進行增強處理。

@Aspect
public class SingleClickAspect {
    private static final String TAG = "SingleClickAspect";

    private long lastClickTime;

    /**
     * 定義切點,標記切點爲所有被 @SingleClick 註解修飾的方法
     */
    @Pointcut("execution(@com.xing.aspectjsample.SingleClick * *(..))")
    void singleClickAnnotated() {

    }

    /**
     * 定義一個 Advice,包裹切點方法
     *
     * @param joinPoint
     */
    @Around("singleClickAnnotated()")
    public void handleSingleClick(ProceedingJoinPoint joinPoint) throws Throwable {
        // 獲取方法參數
        View view = null;
        for (Object arg : joinPoint.getArgs()) {
            if (arg instanceof View) {
                view = (View) arg;
                break;
            }
        }
        if (view == null) {
            return;
        }
        // SingleClick 註解只修飾在方法上
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        // 只處理有 SingleClick 註解修飾的方法
        if (!method.isAnnotationPresent(SingleClick.class)) {
            return;
        }
        // 獲取到 SingleClick 註解對象
        SingleClick singleClick = method.getAnnotation(SingleClick.class);
        // 獲取註解值
        long value = singleClick.value();
        long currentTime = System.currentTimeMillis();
        if (currentTime - lastClickTime > value) {
            lastClickTime = currentTime;
            joinPoint.proceed();
        }
    }
}

Activity 中進行引用:

@SingleClick
public void click(View view) {
	Log.e(TAG, "click: ");
	Toast.makeText(this, "the button is clicked", Toast.LENGTH_SHORT).show();
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章