Android AOP:最簡單&粗暴(使用與原理)講解

1.前言

  • 最近隨著項(xiàng)目迭代業(yè)務(wù)與使用的用戶逐漸增多,陸續(xù)的出現(xiàn)了一些性能或者BUG的問題(例如:1.某些功能執(zhí)行響應(yīng)慢;2.按鍵復(fù)電;等等)。
  • 根據(jù)上述部分性能問題,提出相對應(yīng)的解決方案:
    • 某些功能執(zhí)行響應(yīng)慢 ---> 性能監(jiān)控
    • 按鍵復(fù)點(diǎn) ---> 按鍵防抖
  • 上述2個解決方案的最終實(shí)現(xiàn)原理:就是在執(zhí)行函數(shù)的前或者后臺添加對應(yīng)的邏輯處理。
  • 選擇實(shí)現(xiàn)方式:
    1.直接在方法前后添加對應(yīng)的邏輯。
    2.使用靜態(tài)/動態(tài)代理。
    3.使用AOP。
  • 1與2方式:當(dāng)涉及修改的方法或者類較多時(shí),實(shí)現(xiàn)比較復(fù)雜,而且侵入性大,更不方便后期擴(kuò)展與維護(hù)。
  • 第3方式:侵入性很少;實(shí)現(xiàn)簡單;解耦性很大;易維護(hù)。而這種方式是如何使用?原理是什么?。
  • 今天就為大家揭開AOP的面紗。
  • 文章中實(shí)例 linhaojian的Github

2.介紹

2.1 AOP 是什么

  • AOP為Aspect Oriented Programming的縮寫。意為:面向切面編程。

2.2 AOP 作用

  • 通過預(yù)編譯方式運(yùn)行期動態(tài)代理,來改變原來執(zhí)行結(jié)果的技術(shù)。

2.3 AOP 特點(diǎn)

  • 使原執(zhí)行邏輯與改變執(zhí)行邏輯解耦。
  • 對原代碼侵入性少
  • 容易擴(kuò)展輔助功能(例如:日志)。

2.4 Android 中應(yīng)用

  • 日志
  • 持久化
  • 性能監(jiān)控
  • 數(shù)據(jù)校驗(yàn)
  • 緩存
  • 按鈕防抖
  • 其他更多

2.5 AOP 術(shù)語

  • 通知、增強(qiáng)處理(Advice):就是你想要的功能,也就是上面說的日志、耗時(shí)計(jì)算等。
  • 連接點(diǎn)(JoinPoint):允許你通知(Advice)的地方,那可就真多了,基本每個方法的前、后(兩者都有也行),或拋出異常是時(shí)都可以是連接點(diǎn)(spring只支持方法連接點(diǎn))。AspectJ還可以讓你在構(gòu)造器或?qū)傩宰⑷霑r(shí)都行,不過一般情況下不會這么做,只要記住,和方法有關(guān)的前前后后都是連接點(diǎn)。
  • 切入點(diǎn)(Pointcut):上面說的連接點(diǎn)的基礎(chǔ)上,來定義切入點(diǎn),你的一個類里,有15個方法,那就有十幾個連接點(diǎn)了對吧,但是你并不想在所有方法附件都使用通知(使用叫織入,下面再說),你只是想讓其中幾個,在調(diào)用這幾個方法之前、之后或者拋出異常時(shí)干點(diǎn)什么,那么就用切入點(diǎn)來定義這幾個方法,讓切點(diǎn)來篩選連接點(diǎn),選中那幾個你想要的方法。
  • 切面(Aspect):切面是通知和切入點(diǎn)的結(jié)合?,F(xiàn)在發(fā)現(xiàn)了吧,沒連接點(diǎn)什么事,連接點(diǎn)就是為了讓你好理解切點(diǎn)搞出來的,明白這個概念就行了。通知說明了干什么和什么時(shí)候干(什么時(shí)候通過before,after,around等AOP注解就能知道),而切入點(diǎn)說明了在哪干(指定到底是哪個方法),這就是一個完整的切面定義。
  • 織入(weaving): 把切面應(yīng)用到目標(biāo)對象來創(chuàng)建新的代理對象的過程。

2.6 AOP 注解

  • @Aspect:聲明切面,標(biāo)記類
  • @Pointcut(切點(diǎn)表達(dá)式):定義切點(diǎn),標(biāo)記方法
  • @Before(切點(diǎn)表達(dá)式):前置通知,切點(diǎn)之前執(zhí)行
  • @Around(切點(diǎn)表達(dá)式):環(huán)繞通知,切點(diǎn)前后執(zhí)行
  • @After(切點(diǎn)表達(dá)式):后置通知,切點(diǎn)之后執(zhí)行
  • @AfterReturning(切點(diǎn)表達(dá)式):返回通知,切點(diǎn)方法返回結(jié)果之后執(zhí)行
  • @AfterThrowing(切點(diǎn)表達(dá)式):異常通知,切點(diǎn)拋出異常時(shí)執(zhí)行

@Pointcut、@Before、@Around、@After、@AfterReturning、@AfterThrowing需要在切面類中使用,即在使用@Aspect的類中。

2.7 切點(diǎn)表達(dá)式

AspectJ語法1.png

AspectJ語法2.png

AspectJ語法3.png
  • 示例:
    // 返回類型:*  類名+函數(shù)名:com.lhj.test_apt..*ck(..)   
    //  com.lhj.test_apt..*ck(..)  :意思是com.lhj.test_apt包下所有類中以ck結(jié)尾的函數(shù)
    @Pointcut("execution(*  com.lhj.test_apt..*ck(..))")

3.使用

  • 3.1 項(xiàng)目根目錄(build.gradle)的dependencies中,添加如下代碼:
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.13'    // add
        classpath 'org.aspectj:aspectjrt:1.8.13'       // add
    }
  • 3.2 在主項(xiàng)目或者需要引用的library的build.gradle中,示例如下:
apply plugin: 'com.android.library'
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
android {
    compileSdkVersion 28
    defaultConfig {
        minSdkVersion 17
        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'
        }
    }
}
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // aop
    implementation 'org.aspectj:aspectjrt:1.8.13'
}
project.android.libraryVariants.all { variant ->
    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)]
        MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler)
        def log = project.logger
        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:
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break;
            }
        }
    }
}
  • 3.3 定義切面類(例如:在源碼一些方法上添加額外日志打印):
/**
 * 添加切面注解
 */
@Aspect
public class AopTest {
    private static final String TAG = "linhaojian";
    /**
     * 定義切入點(diǎn)(定義那些類或者方法需要改變)
     */
    @Pointcut("execution(*  com.lhj.test_apt..*ck(..))")
    public void pointcut1() {
    }
    /**
     * 使用注解方式,定義注解
     */
    @Pointcut("execution(@com.lhj.test_apt.DebugLog * *ck(..))")
    public void pointcut() {
    }
    /**
     * 前置通知,切點(diǎn)之前執(zhí)行
     * @param point
     */
    @Before("pointcut()")
    public void logBefore(JoinPoint point){
        Log.e(TAG,"logBefore");
    }
    /**
     * 環(huán)繞通知,切點(diǎn)前后執(zhí)行
     * @param joinPoint
     * @throws Throwable
     */
    @Around("pointcut()")
    public void logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.e(TAG,"logAround");
        // 1.執(zhí)行切點(diǎn)函數(shù)(如果不調(diào)用該方法,切點(diǎn)函數(shù)不被執(zhí)行)
        joinPoint.proceed();
    }
    /**
     * 后置通知,切點(diǎn)之后執(zhí)行
     * @throws Throwable
     */
    @After("pointcut()")
    public void logAfter(JoinPoint point){
        Log.e(TAG,"logAfter");
    }
    /**
     * 返回通知,切點(diǎn)方法返回結(jié)果之后執(zhí)行
     * @throws Throwable
     */
    @AfterReturning("pointcut()")
    public void logAfterReturning(JoinPoint point, Object returnValue){
        Log.e(TAG,"logAfterReturning ");
    }
    /**
     * 異常通知,切點(diǎn)拋出異常時(shí)執(zhí)行
     * @throws Throwable
     */
    @AfterThrowing(value = "pointcut()",throwing = "ex")
    public void logAfterThrowing(Throwable ex){
        Log.e(TAG,"logAfterThrowing : "+ex.getMessage());
    }
}
  • 注意:注釋1中,如果使用了@Around注釋,記得調(diào)用joinPoint.proceed(),不然切點(diǎn)的函數(shù)是不會被調(diào)用的。
  • 3.4 切點(diǎn)的使用方式:
  • 無侵入式@Pointcut定義不包含注解時(shí),就能實(shí)現(xiàn)源代碼零修改的改變。
  • 低侵入式@Pointcut定義包含注解時(shí),就需要在源代碼的函數(shù)中添加相應(yīng)的注解,如下代碼塊。
public class AopActivity extends AppCompatActivity {
    private Button button;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.aop);
        button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                click();
            }
        });
    }
    @DebugLog
    public void click(){
        Log.e("linhaojian","onClick()");
    }
}
  • DebugLog注解代碼
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DebugLog {
}
  • 3.5 運(yùn)行結(jié)果


    執(zhí)行結(jié)果.png

4.原理

  • 在分析其原理前,我們先看看包含切點(diǎn)功能的類,編譯之后的class文件內(nèi)容:
public class AopActivity extends AppCompatActivity {
    private Button button;
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(layout.aop);
        this.button = (Button)this.findViewById(id.button);
        this.button.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                AopActivity.this.click();
            }
        });
    }
    @DebugLog
    public void click() {
        // 1
        JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
        try {
            try {
                // 2
                AopTest.aspectOf().logBefore(var1);
               // 3
                click_aroundBody1$advice(this, var1, AopTest.aspectOf(), (ProceedingJoinPoint)var1);
            } catch (Throwable var4) {
                AopTest.aspectOf().logAfter(var1);
                throw var4;
            }
            AopTest.aspectOf().logAfter(var1);
            AopTest.aspectOf().logAfterReturning(var1, (Object)null);
        } catch (Throwable var5) {
            AopTest.aspectOf().logAfterThrowing(var5);
            throw var5;
        }
    }
}
@Aspect
public class AopTest {
    //...
    public static AopTest aspectOf() {
        if (ajc$perSingletonInstance == null) {
            throw new NoAspectBoundException("com.lhj.test_apt.AopTest", ajc$initFailureCause);
        } else {
            return ajc$perSingletonInstance;
        }
    }
    public static boolean hasAspect() {
        return ajc$perSingletonInstance != null;
    }
    static {
        try {
            // 4
            ajc$postClinit();
        } catch (Throwable var1) {
            ajc$initFailureCause = var1;
        }
    }
}
  • 從上面編譯后的class文件,可以看很清楚編譯時(shí)改變了原方法的執(zhí)行流程或結(jié)果,添加了切面類中定義的一些額外執(zhí)行邏輯(例如:logBefore、logAfter);
  • 注釋1:通過工廠類,創(chuàng)建ProceedingJoinPoint的實(shí)例;
  • 注釋2:AopTest.aspectOf()代表獲取AopTest的單例(單例時(shí)在注釋4中通過static塊初始化);
  • 注釋3:其實(shí)就是將原方法的執(zhí)行內(nèi)容封裝至ProceedingJoinPoint,使它們之間關(guān)聯(lián)。

5.總結(jié)

  • 到此,AOP就結(jié)束完畢。
  • 如果喜歡我的分享,可以點(diǎn)擊 關(guān)注 或者 ,你們支持是我分享的最大動力 。
  • linhaojian的Github

歡迎關(guān)注linhaojian_CSDN博客或者linhaojian_簡書!

不定期分享關(guān)于安卓開發(fā)的干貨。


寫技術(shù)文章初心

  • 技術(shù)知識積累
  • 技術(shù)知識鞏固
  • 技術(shù)知識分享
  • 技術(shù)知識交流
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容