Android面向切面編程(AOP)

一、簡述

1.AOP概念

AOP為Aspect Oriented Programming的縮寫朗兵,意為:面向切面編程污淋,通過預(yù)編譯方式和運行期動態(tài)代理實現(xiàn)程序功能的統(tǒng)一維護的一種技術(shù)。一句話概括:在運行時余掖,動態(tài)地將代碼切入到類的指定方法、指定位置上的編程思想就是面向切面的編程礁鲁。

2.運用場景

項目開發(fā)過程中盐欺,可能會有這樣的需求,需要我們在方法執(zhí)行完成后仅醇,記錄日志(后臺開發(fā)中比較常見~)冗美,或是計算這個方法的執(zhí)行時間,在不使用AOP的情況下析二,我們可以在方法最后調(diào)用另一個專門記錄日志的方法粉洼,或是在方法體的首尾分別獲取時間,然后通過計算時間差來計算整個方法執(zhí)行所消耗的時間叶摄,這樣也可以完成需求属韧。那如果不只一個方法要這么玩怎么辦?每個方法都寫上一段相同的代碼嗎蛤吓?后期處理邏輯變了要怎么辦宵喂?其實這種問題我們完全可以用AOP來解決。

3.AOP的實現(xiàn)方式

AOP僅僅只是個概念会傲,實現(xiàn)它的方式(工具和庫)有以下幾種:

  • AspectJ: 一個 JavaTM 語言的面向切面編程的無縫擴展(適用Android)锅棕。
  • Javassist for Android: 用于字節(jié)碼操作的知名 java 類庫 Javassist 的 Android 平臺移植版。
  • DexMaker: Dalvik 虛擬機上淌山,在編譯期或者運行時生成代碼的 Java API裸燎。
  • ASMDEX: 一個類似 ASM 的字節(jié)碼操作庫,運行在Android平臺泼疑,操作Dex字節(jié)碼德绿。

下面就來看看AspectJ方式的AOP如何在Android開發(fā)中進行使用的。

二王浴、AspectJ導入

項目gradle添加

buildscript {
    repositories {
        google()
        jcenter()
        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.4.0'
        classpath 'org.aspectj:aspectjtools:1.9.4'
        
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

主model添加依賴

dependencies {
    ...
     implementation 'org.aspectj:aspectjrt:1.9.4'
}

添加gradle任務(wù),直接粘貼到build.gradle文件的末尾即可脆炎,不要嵌套在別的指令中。


import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

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

//在構(gòu)建工程時氓辣,執(zhí)行編輯
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.9",
                         "-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;
            }
        }
    }
}

三秒裕、AspectJ基礎(chǔ)知識

在這里插入圖片描述
AOP注解

四、AspectJ實現(xiàn)AOP

1.創(chuàng)建自定義注解

用注解來標記切點钞啸,一般會使用自定義注解几蜻,方便我們拓展喇潘。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnoTrace {
    String value();
    int type();
}
  • @Target(ElementType.METHOD):表示該注解只能注解在方法上。如果想類和方法都可以用梭稚,那可以這么寫:@Target({ElementType.METHOD,ElementType.TYPE})颖低,依此類推。
  • @Retention(RetentionPolicy.RUNTIME):表示該注解在程序運行時是可見的(還有SOURCE弧烤、CLASS分別指定注解對于那個級別是可見的忱屑,一般都是用RUNTIME)。

其中的value和type是自己拓展的屬性暇昂,方便存儲一些額外的信息莺戒。

2.使用自定義注解標記切點

這個自定義注解只能注解在方法上(構(gòu)造方法除外,構(gòu)造方法也叫構(gòu)造器急波,需要使用ElementType.CONSTRUCTOR)从铲,像平常使用其它注解一樣使用它即可:

@TestAnnoTrace(value = "test", type = 1)
public void test(View view) {
    System.out.println("Hello");
}

3.創(chuàng)建切面類

切點表達式的組成如下:

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

要織入一段代碼到目標類方法的前前后后,必須要有一個切面類澄暮,下面就是切面類的代碼:

@Aspect
public class TestAnnoAspect {

    //切點表達式使用自定義的注解名段,一定是@+注解全路徑
    @Pointcut("execution(@com.hxl.androidaopdemo.TestAnnoTrace * *(..))")
    public void pointcut() {}
    
    @Before("pointcut()")
    public void before(JoinPoint point) {
        System.out.println("@Before");
    }

    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("@Around");
        joinPoint.proceed();// 目標方法執(zhí)行完畢
    }

    @After("pointcut()")
    public void after(JoinPoint point) {
        System.out.println("@After");
    }

    @AfterReturning("pointcut()")
    public void afterReturning(JoinPoint point, Object returnValue) {
        System.out.println("@AfterReturning");
    }

    @AfterThrowing(value = "pointcut()", throwing = "ex")
    public void afterThrowing(Throwable ex) {
        System.out.println("@afterThrowing");
        System.out.println("ex = " + ex.getMessage());
    }
}

打印結(jié)果:

@Before
@Around
Hello
@After
@AfterReturning

少了一個@AfterThrowing通知。這個通知只有在切點拋出異常時才會執(zhí)行

4.JoinPoint的作用

發(fā)現(xiàn)沒有泣懊,上面所有的通知都會至少攜帶一個JointPoint參數(shù)伸辟,這個參數(shù)包含了切點的所有信息,下面就結(jié)合按鈕的點擊事件方法test()來解釋joinPoint能獲取到的方法信息有哪些:

MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String name = signature.getName(); // 方法名:test
Method method = signature.getMethod(); // 方法:public void com.hxl.androidaopdemo.MainActivity.test(android.view.View)
Class returnType = signature.getReturnType(); // 返回值類型:void
Class declaringType = signature.getDeclaringType(); // 方法所在類名:MainActivity
String[] parameterNames = signature.getParameterNames(); // 參數(shù)名:view
Class[] parameterTypes = signature.getParameterTypes(); // 參數(shù)類型:View

//上面在編寫自定義注解時就聲明了兩個屬性嗅定,分別是value和type自娩,
//通過Method對象得到切點上的注解
TestAnnoTrace annotation = method.getAnnotation(TestAnnoTrace.class);
String value = annotation.value();
int type = annotation.type();

5.方法耗時計算的實現(xiàn)

因為@Around是環(huán)繞通知,可以在切點的前后分別執(zhí)行一些操作渠退,AspectJ為了能肯定操作是在切點前還是在切點后忙迁,所以在@Around通知中需要手動執(zhí)行joinPoint.proceed()來確定切點已經(jīng)執(zhí)行,故在joinPoint.proceed()之前的代碼會在切點執(zhí)行前執(zhí)行碎乃,在joinPoint.proceed()之后的代碼會切點執(zhí)行后執(zhí)行姊扔。于是,方法耗時計算的實現(xiàn)就是這么簡單:

@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
    long beginTime = SystemClock.currentThreadTimeMillis();
    joinPoint.proceed();
    long endTime = SystemClock.currentThreadTimeMillis();
    long dx = endTime - beginTime;
    System.out.println("耗時:" + dx + "ms");
}

五梅誓、動態(tài)代理實現(xiàn)AOP

動態(tài)代理也叫做JDK代理恰梢、接口代理。不需要實現(xiàn)目標對象的接口梗掰。生成代理對象嵌言,使用的是Java的API,動態(tài)的在內(nèi)存中構(gòu)件代理對象(這需要我們指定創(chuàng)建代理對象/目標對象的接口的類型)及穗。

在java的動態(tài)代理機制中摧茴,有兩個重要的類或接口,一個是 InvocationHandler(Interface)埂陆、另一個則是Proxy(Class)苛白,這一個類和接口是實現(xiàn)我們動態(tài)代理所必須用到的娃豹。

InvocationHandler

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

每一個代理實類例的invocation handler都要實現(xiàn)InvocationHandler這個接口。并且每個代理類的實例都關(guān)聯(lián)到了一個handler购裙,當我們通過代理對象調(diào)用一個方法的時候懂版,這個方法的調(diào)用就會被轉(zhuǎn)發(fā)為由InvocationHandler這個接口的 invoke 方法來進行調(diào)用。

  • proxy:指代生成的代理對象躏率;
  • method:指代的是我們所要調(diào)用真實對象的某個方法的Method對象躯畴;
  • args:指代的是調(diào)用真實對象某個方法時接受的參數(shù);

我們來看看Proxy這個類薇芝,這個類的作用就是用來動態(tài)創(chuàng)建一個代理對象的類私股,它提供了許多的方法,但用的最多的就是 newProxyInstance 這個方法:

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
  • loader:一個ClassLoader對象恩掷,定義了由哪個ClassLoader對象來對生成的代理對象進行加載
  • interfaces:一個Interface對象的數(shù)組吴攒,表示的是我將要給我需要代理的對象提供一組什么接口瑟匆,如果我提供了一組接口給它,那么這個代理對象就宣稱實現(xiàn)了該接口(多態(tài))天通,這樣我就能調(diào)用這組接口中的方法了
  • h:一個InvocationHandler對象克滴,表示的是當我這個動態(tài)代理對象在調(diào)用方法的時候逼争,會關(guān)聯(lián)到哪一個InvocationHandler對象上。

通過上面的需要傳入接口的參數(shù)可以看出劝赔,JDK動態(tài)代理需要借助接口來實現(xiàn)誓焦,如果我們要代理的對象功能沒有抽成任何接口,那么我們就無法通過JDK動態(tài)代理的方式來實現(xiàn)着帽。

好了杂伟,在介紹完這兩個接口(類)以后,我們來通過一個實例來看看我們的動態(tài)代理模式是什么樣的仍翰。首先我們定義了一個Subject類型的接口赫粥,為其聲明了兩個方法,這兩個方法表示被代理類需要實現(xiàn)的功能:

public interface Subject {
    public void sayGoodBye();
    public void sayHello(String str);
}

接著予借,定義了一個類來實現(xiàn)這個接口越平,這個類就是我們的真實對象(被代理類),RealSubject類:

public class RealSubject implements Subject {
    @Override
    public void sayGoodBye() {
        System.out.println("RealSubject sayGoodBye");
    }

    @Override
    public void sayHello(String str) {
        System.out.println("RealSubject sayHello  " + str);
    }
}

下一步灵迫,我們就要定義一個InvocationHandler了秦叛,相當于一個代理處理器。前面說個瀑粥,每一個動態(tài)代理類實例的invocation handler 都必須要實現(xiàn) InvocationHandler 這個接口:

public class SubjectInvocationHandler implements InvocationHandler {
    //這個就是我們要代理的真實對象
    private Object subject;

    //構(gòu)造方法挣跋,給我們要代理的真實對象賦初值
    public SubjectInvocationHandler(Object subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object object, Method method, Object[] args) throws Throwable {
        //在代理真實對象前我們可以添加一些自己的操作
        System.out.println("before Method invoke");
        System.out.println("Method:" + method);
        //當代理對象調(diào)用真實對象的方法時,其會自動的跳轉(zhuǎn)到代理對象關(guān)聯(lián)的handler對象的invoke方法來進行調(diào)用
        method.invoke(subject, args);
        //在代理真實對象后我們也可以添加一些自己的操作
        System.out.println("after Method invoke");
        return null;
    }
}

SubjectInvocationHandler并不是真正的代理類利凑,而是用于定義代理類需要擴展浆劲、增強那些方法功能的類嫌术。在invoke函數(shù)中,對代理對象的所有方法的調(diào)用都被轉(zhuǎn)發(fā)至該函數(shù)處理牌借。在這里可以靈活的自定義各種你能想到的邏輯度气。

最后,來看看我們的Client類:

 public static void main(String[] args) {
       //被代理類
       Subject realSubject = new RealSubject();
       //我們要代理哪個類膨报,就將該對象傳進去磷籍,最后是通過該被代理對象來調(diào)用其方法的
       SubjectInvocationHandler handler = new SubjectInvocationHandler(realSubject);
       //生成代理類
       Subject subject = (Subject) Proxy.newProxyInstance(handler.getClass().getClassLoader(), 
                                                          realSubject.getClass().getInterfaces(), handler);
       //輸出代理類對象
       System.out.println("Proxy : "+ subject.getClass().getName());
       System.out.println("Proxy super : "+ subject.getClass().getSuperclass().getName());
       System.out.println("Proxy interfaces : "+ subject.getClass().getInterfaces()[0].getName());
       //調(diào)用代理類sayGoodBye方法
       subject.sayGoodBye();
       System.out.println("--------");
       //調(diào)用代理類sayHello方法
       subject.sayHello("Test");
   }

輸出結(jié)果:

Proxy : com.sun.proxy.$Proxy0
Proxy super : java.lang.reflect.Proxy
Proxy interfaces : com.company.ha.Subject
before Method invoke
Method:public abstract void com.company.ha.Subject.sayGoodBye()
RealSubject sayGoodBye
after Method invoke
--------
before Method invoke
Method:public abstract void com.company.ha.Subject.sayHello(java.lang.String)
RealSubject sayHello  Test
after Method invoke

如果我們定義的方法有返回值,那么可以通過invoke中把該方法的返回值進行返回现柠,因為返回值的對象是Object院领,所以支持返回值為空(void)的返回。

 @Override
public Object invoke(Object object, Method method, Object[] args) throws Throwable {
    //直接返回調(diào)用對象的返回值
    return method.invoke(subject,args);
}

六够吩、動態(tài)代理應(yīng)用(Retrofit)

Android中的網(wǎng)絡(luò)注解框架retrofit內(nèi)部實現(xiàn)其實就是應(yīng)用了動態(tài)代理技術(shù)比然,通常我們定義網(wǎng)絡(luò)接口是這樣的:

public interface ApiStore {

    // 員工登錄
    @FormUrlEncoded
    @POST("/resource/d/member/login")
    Observable<BaseResponse<LoginResult>> login(@FieldMap Map<String, String> params);

    // 退出登錄
    @FormUrlEncoded
    @POST("/resource/d/member/signOut")
    Observable<BaseResponse<LogOutResult>> logout(@FieldMap Map<String, String> params);
    
    //....
}

創(chuàng)建Retrofit:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://ww.xxx.com/")
    .build();
ApiStore service = retrofit.create(ApiStore.class);

我們可以看retrofit.create內(nèi)部具體的實現(xiàn)方式:

public <T> T create(final Class<T> service) {
    //...
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          private final Platform platform = Platform.get();

          @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
              throws Throwable {
            // If the method is a method from Object then defer to normal invocation.
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            ServiceMethod<Object, Object> serviceMethod =
                (ServiceMethod<Object, Object>) loadServiceMethod(method);
            OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
  }

看到?jīng)],retrofit就是通過Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },new InvocationHandler() )的方式創(chuàng)建的一個代理類周循,invoke中的代碼就是當網(wǎng)絡(luò)接口被調(diào)用的時候需要做的處理强法。在這個方法里,首先根據(jù)接口定義的方法湾笛,生成一個ServiceMethod對象饮怯,在ServiceMethod對象中會反射接口中定義的注解,解析出具體的網(wǎng)絡(luò)請求方式嚎研,然后拿到封裝好的ServiceMethod對象后蓖墅,構(gòu)造一個OkHttpCall對象,以便與進行真正的網(wǎng)絡(luò)請求(OkHttp)临扮。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末论矾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子公条,更是在濱河造成了極大的恐慌拇囊,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件靶橱,死亡現(xiàn)場離奇詭異寥袭,居然都是意外死亡,警方通過查閱死者的電腦和手機关霸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門传黄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人队寇,你說我怎么就攤上這事膘掰。” “怎么了?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵识埋,是天一觀的道長凡伊。 經(jīng)常有香客問我,道長窒舟,這世上最難降的妖魔是什么系忙? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮惠豺,結(jié)果婚禮上银还,老公的妹妹穿的比我還像新娘。我一直安慰自己洁墙,他們只是感情好蛹疯,可當我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著热监,像睡著了一般捺弦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上孝扛,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天羹呵,我揣著相機與錄音,去河邊找鬼疗琉。 笑死,一個胖子當著我的面吹牛歉铝,可吹牛的內(nèi)容都是我干的盈简。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼太示,長吁一口氣:“原來是場噩夢啊……” “哼柠贤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起类缤,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤臼勉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后餐弱,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體宴霸,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年膏蚓,在試婚紗的時候發(fā)現(xiàn)自己被綠了瓢谢。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡驮瞧,死狀恐怖氓扛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情论笔,我是刑警寧澤采郎,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布千所,位于F島的核電站,受9級特大地震影響蒜埋,放射性物質(zhì)發(fā)生泄漏淫痰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一理茎、第九天 我趴在偏房一處隱蔽的房頂上張望黑界。 院中可真熱鬧,春花似錦皂林、人聲如沸朗鸠。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烛占。三九已至,卻和暖如春沟启,著一層夾襖步出監(jiān)牢的瞬間忆家,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工德迹, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留芽卿,地道東北人。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓胳搞,卻偏偏與公主長得像卸例,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子肌毅,可洞房花燭夜當晚...
    茶點故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容