AOP開發(fā)——AspectJ的使用

文章對(duì)應(yīng)的項(xiàng)目地址aop-tech,運(yùn)行一下sample,結(jié)合代碼和文章,你會(huì)收獲更多。

熟悉程序開發(fā)的都知道OOP(Object Oriented Programming ,面向?qū)ο缶幊蹋压δ芊庋b在一個(gè)類中,使用的時(shí)候創(chuàng)建該類的對(duì)象,調(diào)用對(duì)象的方法或者使用其屬性即可,OOP具有可重用性、靈活性和擴(kuò)展性。
盡管OOP具有很多好處,但是如果在軟件開發(fā)領(lǐng)域只使用OOP,在某些情況下也會(huì)使程序變得復(fù)雜且難以維護(hù)。例如,我們需要統(tǒng)計(jì)程序中點(diǎn)擊事件的執(zhí)行情況,如果我們要自己找遍代碼中的點(diǎn)擊事件,這個(gè)工程量就太大了,而且維護(hù)起來也不方便。這個(gè)時(shí)候,使用AOP的方式就會(huì)使問題變得簡(jiǎn)單。
AOP(Aspect Oriented Programming,面向切面編程),把某一類問題集中在一個(gè)地方進(jìn)行處理,比如處理程序中的點(diǎn)擊事件、打印日志等。

關(guān)于OOP和AOP,我覺得鄧凡平老師在深入理解Android之AOP中說的挺對(duì)的:

OOP和AOP都是方法論,表示的是我們從什么角度來看待問題。OOP的精髓是把功能或問題模塊化,每個(gè)模塊處理自己的家務(wù)事。但在現(xiàn)實(shí)世界中,并不是所有功能都能完美得劃分到模塊中。AOP的目標(biāo)是把這些功能集中起來,放到一個(gè)統(tǒng)一的地方來控制和管理。

那么在Android中有哪些使用到了AOP這種思想呢?
在Application中有個(gè)ActivityLifecycleCallbacks接口,這個(gè)接口提供了Activity生命周期相關(guān)的方法回調(diào)。當(dāng)開發(fā)者調(diào)用了Application的public void registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback) 方法之后,就可以在ActivityLifecycleCallbacks的實(shí)現(xiàn)類中統(tǒng)一處理這些生命周期方法。這其實(shí)就是AOP思想的一種體現(xiàn)。

ActivityLifecycleCallbacks的AOP思想.png

另外,我們今天的主角——AspectJ, 它是AOP編程思想的一個(gè)很火的實(shí)踐。

AspectJ 介紹

AspectJ是一個(gè)面向切面編程的框架,它擴(kuò)展了Java語言。AspectJ定義了AOP語法,它有一個(gè)專門的編譯器用來生成遵守Java字節(jié)編碼規(guī)范的Class文件。AspectJ還支持原生的Java,只需要加上AspectJ提供的注解即可。在Android開發(fā)中,一般就用它提供的注解和一些簡(jiǎn)單的語法就可以實(shí)現(xiàn)絕大部分功能上的需求了。

Join Points介紹 **
Join Points,簡(jiǎn)稱JPoints,是AspectJ中最關(guān)鍵的一個(gè)概念,表示的是程序運(yùn)行時(shí)的一些
執(zhí)行點(diǎn)**。理論上說,一個(gè)程序中很多地方都可以被看做是JPoint,但是AspectJ中,只有幾種執(zhí)行點(diǎn)被認(rèn)為是JPoints,如構(gòu)造方法調(diào)用、方法調(diào)用、方法執(zhí)行、異常等等。JPoints實(shí)際上就是表示想把AspectJ的代碼插入到程序哪個(gè)地方,是插入在方法中,還是插入在方法調(diào)用前后。需要說明的是:在AspectJ中,方法調(diào)用(call)和方法執(zhí)行(execution)是不一樣的,這個(gè)后面再做介紹。

Pointcuts介紹
一個(gè)程序會(huì)有很多的JPoints,即使是同一個(gè)函數(shù),還分為call類型和execution類型的JPoint,但是并不是所有的JPoint都是我們需要關(guān)心的。比如我們可能只需要關(guān)心點(diǎn)擊事件方法,那么如何從眾多的JPoints中選擇我們感興趣的JPoint呢?這個(gè)時(shí)候可以用Pointcut:

@Around("execution(* android.view.View.OnClickListener.onClick(..))")
public void onClickMethodAround(ProceedingJoinPoint joinPoint)  {}

上述代碼的意思就是在OnClickListener.onClick()方法執(zhí)行前后執(zhí)行代碼塊中的邏輯。

所以在這里,我們可以簡(jiǎn)單的理解Pointcut的作用就是過濾JPoint。

Advice介紹
Advice簡(jiǎn)單來說就是表示AspectJ的hook點(diǎn),在AspectJ中常用的是before、after、around等。before表示在JPoint執(zhí)行之前,需要干的事情。after表示的是在JPoint執(zhí)行之后,around表示的是在JPoint執(zhí)行前后。

Aspect介紹
前面我們講了AspectJ中使用過程中需要用到了一個(gè)概念,對(duì)于問題的處理需要統(tǒng)一放到一個(gè)地方去處理,這個(gè)地方就是Aspect,意為“切面”。在Java開發(fā)中主要是使用@Aspect注解來表示一個(gè)切面。

Android 中使用Gradle集成 AspectJ

在Android中集成AspectJ,主要思想就是hook Apk打包過程,使用AspectJ提供的工具來編譯.class文件。這一點(diǎn),JakeWharton 在其項(xiàng)目JakeWharton/hugo 中演示了如何在Gradle中添加AspectJ,這為后來的人指了一條光明的道路。

一般來說,自己手動(dòng)接入AspectJ的話,按照下面的指示即可。

在項(xiàng)目根目錄build.gradle下引入aspectjtools插件:

buildscript {
    dependencies {
        ..
        classpath 'org.aspectj:aspectjtools:1.8.10'
        classpath 'org.aspectj:aspectjweaver:1.8.8'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

在運(yùn)行app的module目錄下的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

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;
            }
        }
    }
}

AspectJ在運(yùn)行時(shí)也需要相關(guān)的Library支持,所以還需要在項(xiàng)目的dependencies中添加依賴:

dependencies {
   ...
  compile 'org.aspectj:aspectjrt:1.8.10'
}

目前還有一些在Android中集成AspectJ的比較火的框架,如 HujiangTechnology / gradle_plugin_android_aspectjx。該框架支持kotlin,我對(duì)這個(gè)框架深入研究了一番,也按照它的思想寫了一個(gè)簡(jiǎn)單的gradle plugin ,收獲頗多,我自己的項(xiàng)目地址是 aop-tech,項(xiàng)目中演示了如何通過AOP的方式解決統(tǒng)一處理登錄、綁定手機(jī)號(hào)、統(tǒng)計(jì)方法耗時(shí)、打印點(diǎn)擊事件日志等的邏輯,有興趣的可以去看看,歡迎交流。

AspectJ 命令常用參數(shù)介紹

1 -inpath: .class文件路徑,可以是在jar文件中也可以是在文件目錄中,路徑應(yīng)該包含那些AspectJ相關(guān)的文件,只有這些文件才會(huì)被AspectJ處理。輸出文件會(huì)包含這些.class 。該路徑就是一個(gè)單一參數(shù),多個(gè)路徑的話用分隔符隔開。

2 -classpath: 指定去哪找用戶使用到的.class文件,路徑可以是zip文件也可以是文件目錄,該路徑就是一個(gè)單一參數(shù),多個(gè)路徑的話用分隔符隔開。

3 -aspectPath: 需要被處理的切面路徑,存在于jar文件或者文件目錄中。在Andorid中使用的話一般指的是被@Aspect注解標(biāo)示的class文件路徑。需要注意的是編譯版本需要與Java編譯版本一致。classpath指定的路徑應(yīng)該包含所有的aspectpath指定的.class文件。不過默認(rèn)情況下,inPath和aspectPath中的路徑不一定非要放置在classPath中,因?yàn)榫幾g器會(huì)自動(dòng)處理把它們加入。路徑格式與classpath和inpath樣,都需要用分隔符隔開。

4 **-bootClasspath: ** 重載跟VM相關(guān)的bootClasspath,例如在Android中使用android-27的源碼進(jìn)行編譯。路徑格式與之前一樣。

5 -d: 指定由AspectJ處理后的.class文件存放目錄,如果不指定的話會(huì)放置在當(dāng)前的工作目錄中。

6 -outjar: 指定被AspectJ處理后的jar包存放的文件目錄,

更多詳情請(qǐng)查看官網(wǎng) http://www.eclipse.org/aspectj/doc/released/devguide/ajc-ref.html

Sample—處理點(diǎn)擊事件

例如,我們需要處理項(xiàng)目中的所有控件的點(diǎn)擊事件,打印控件的名稱,可以使用AspectJ來簡(jiǎn)單方便的處理。在之前已經(jīng)在gradle中引入的AspectJ的基礎(chǔ)上,我們新建一個(gè)Java文件,如下:

@Aspect
public class ClickAspect {
    private static final String TAG = "ClickAspect";
    // 第一個(gè)*所在的位置表示的是返回值,*表示的是任意的返回值,
    // onClick()中的 .. 所在位置是方法參數(shù)的位置,.. 表示的是任意類型、任意個(gè)數(shù)的參數(shù)
    // * 表示的是通配
    @Pointcut("execution(* android.view.View.OnClickListener.onClick(..))")
    public void clickMethod() {}

    @Around("clickMethod()")
    public void onClickMethodAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object[] args = joinPoint.getArgs();
        View view = null;
        for (Object arg : args) {
            if (arg instanceof View) {
                view = (View) arg;
            }
        }
        //獲取View 的 string id
        String resEntryName = null;
        String resName = null;
        if (view != null) {
            // resEntryName: btn_activity_2  resName: com.sososeen09.aop_tech:id/btn_activity_2
            resEntryName = view.getContext().getResources().getResourceEntryName(view.getId());
            resName = view.getContext().getResources().getResourceName(view.getId());
        }
        joinPoint.proceed();
        Log.d(TAG, "after onclick: " + "resEntryName: " + resEntryName + "  resName: " + resName);
    }
}

運(yùn)行項(xiàng)目,點(diǎn)擊一個(gè)控件(設(shè)置了點(diǎn)擊事件)之后,可以看到日志輸出:

./com.sososeen09.aop_tech D/ClickAspect: after onclick: resEntryName: btn_activity_3 resName: com.sososeen09.aop_tech:id/btn_activity_3

切入點(diǎn)的語法

以上面的例子來講解:

  • @Around:是advice,也就是具體的插入點(diǎn)。@Around該方法的邏輯會(huì)包含切入點(diǎn)前后,如果用到該注解,記得自己需要控制切入點(diǎn)的執(zhí)行邏輯,調(diào)用joinPoint.proceed()。如果使用@Before注解,表示的是在切入點(diǎn)之前執(zhí)行,@After表示在切入點(diǎn)之后執(zhí)行,此時(shí)不需要調(diào)用joinPoint.proceed()
  • execution:處理JPoint的類型,例如call、execution。對(duì)于execution(* android.view.View.OnClickListener.onClick(..)),第一個(gè) * 所處的位置表示的是返回值,* 是通配符,表示的是任意類型。 android.view.View.OnClickListener.onClick(..) 表示的執(zhí)行OnClickListener的onClick()方法。onClick(..)中的.. 表示任意類型、任意個(gè)數(shù)的參數(shù)。
  • onClickMethodAround:表示的實(shí)際切入代碼。這個(gè)方法名可以自己隨意定義。

在上面的例子中實(shí)際上我是自定義了一個(gè)PointCut,名字是clickMethod()。這個(gè)名稱隨意,只要在advice中指定好該名稱就可以了。

@Pointcut("execution(* android.view.View.OnClickListener.onClick(..))")
public void clickMethod() {}

如果不想自定義,可以直接這樣:

@Around("execution(* android.view.View.OnClickListener.onClick(..))")
public void onClickMethodAround(ProceedingJoinPoint joinPoint) throws Throwable {
   ...
}

call和execution

我們之前講的切入點(diǎn)語法都是execution,那么如果使用call有什么區(qū)別呢?

我們?cè)偈褂靡粋€(gè)例子,創(chuàng)建一個(gè)切面用來打印方法的執(zhí)行時(shí)間,并且只處理帶有注解的參數(shù)。
TimeSpend 注冊(cè)如下,value表示的是方法的功能

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TimeSpend {
    String value() default "";
}

使用execution打印方法執(zhí)行時(shí)間的切面如下:

@Aspect
public class MethodSpendTimeAspect {
    private static final String TAG = "MethodSpendTimeAspect";
    @Pointcut("execution(@com.sososeen09.aop_tech.aspect.TimeSpend * *(..))")
    public void methodTime() {}

    @Around("methodTime()")
    public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String className = methodSignature.getDeclaringType().getSimpleName();
        String methodName = methodSignature.getName();
        String funName = methodSignature.getMethod().getAnnotation(TimeSpend.class).value();
        //統(tǒng)計(jì)時(shí)間
        long begin = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long duration = System.currentTimeMillis() - begin;
        Log.e(TAG, String.format("功能:%s,%s類的%s方法執(zhí)行了,用時(shí)%d ms", funName, className, methodName, duration));
        return result;
    }
}

原始Java文件如下:

public class LoginActivity extends AppCompatActivity {
   ...
    @TimeSpend("登錄")
    private void attemptLogin() {
        StatusHolder.sHasLogin = true;
        Toast.makeText(this, "登錄成功", Toast.LENGTH_SHORT).show();
        finish();
    }
}

編譯之后的.class文件:

public class LoginActivity extends AppCompatActivity {
    protected void onCreate(Bundle savedInstanceState) {
...
        super.onCreate(savedInstanceState);
        mEmailSignInButton.setOnClickListener(new OnClickListener() {
            public void onClick(View view) {
                LoginActivity.this.attemptLogin();
            }
        });
    }

    @TimeSpend("登錄")
    private void attemptLogin() {
        JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
        attemptLogin_aroundBody1$advice(this, var1, MethodSpendTimeAspect.aspectOf(), (ProceedingJoinPoint)var1);
    }

    static {
        ajc$preClinit();
    }
}

如果把execution該為call,在看一下編譯后的 .class 文件 :

public class LoginActivity extends AppCompatActivity {
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        mEmailSignInButton.setOnClickListener(new View.OnClickListener() {
            public void onClick(View view) {
                LoginActivity.access$000(com.sososeen09.aop_tech.LoginActivity.this);
            }
        });
    }

    @TimeSpend("登錄")
    private void attemptLogin() {
        StatusHolder.sHasLogin = true;
        Toast.makeText(this, "登錄成功", 0).show();
        this.finish();
    }

    static {
        ajc$preClinit();
    }
    
    static void access$000(LoginActivity x0) {
        JoinPoint makeJP = Factory.makeJP(ajc$tjp_0, null, x0);
        attemptLogin_aroundBody1$advice(x0, makeJP, MethodSpendTimeAspect.aspectOf(), (ProceedingJoinPoint) makeJP);
    }
}

看到區(qū)別了吧,execution表示JPoint是執(zhí)行方法的地方,AspectJ會(huì)對(duì)被執(zhí)行方法做處理。而call表示JPoint是調(diào)用方法的地方,AspectJ會(huì)對(duì)調(diào)用處做處理。

總結(jié)

本文介紹了AOP的一些概念性的知識(shí),簡(jiǎn)單介紹了AspectJ在Android開發(fā)中的基本使用方式。限于篇幅和水平,難以對(duì)AspectJ做一個(gè)全面的介紹,建議對(duì)AOP和AspectJ有興趣的讀者可以閱讀下面的相關(guān)項(xiàng)目和文章,也歡迎交流。

相關(guān)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評(píng)論 6 535
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,744評(píng)論 3 421
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評(píng)論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評(píng)論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,935評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,325評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評(píng)論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,534評(píng)論 0 289
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,084評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,892評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,067評(píng)論 1 371
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評(píng)論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,322評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評(píng)論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評(píng)論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,800評(píng)論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,084評(píng)論 2 375

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,677評(píng)論 25 708
  • 即使是白天,這個(gè)不到20平方米的房間里還是有點(diǎn)暗,里面簡(jiǎn)單地?cái)[放了些家具,木質(zhì)的地板上已經(jīng)有了擦不掉的污痕。房間的...
    新日暮里渣胖閱讀 388評(píng)論 2 1
  • 有一種愛,沒有痕跡…… 她是不經(jīng)意間灑在你臉上的陽光;它是不經(jīng)意間滴落在你肩上的雨滴; 她是你來這個(gè)世界上時(shí)最開心...
    遠(yuǎn)明春閱讀 236評(píng)論 0 0