看AspectJ在Android中的強勢插入

看AspectJ在Android中的強勢插入

什么是AOP

AOP是Aspect Oriented Programming的縮寫,即『面向切面編程』。它和我們平時接觸到的OOP都是編程的不同思想,OOP,即『面向對象編程』,它提倡的是將功能模塊化,對象化,而AOP的思想,則不太一樣,它提倡的是針對同一類問題的統一處理,當然,我們在實際編程過程中,不可能單純的安裝AOP或者OOP的思想來編程,很多時候,可能會混合多種編程思想,大家也不必要糾結該使用哪種思想,取百家之長,才是正道。

那么AOP這種編程思想有什么用呢,一般來說,主要用于不想侵入原有代碼的場景中,例如SDK需要無侵入的在宿主中插入一些代碼,做日志埋點、性能監控、動態權限控制、甚至是代碼調試等等。

AspectJ

AspectJ實際上是對AOP編程思想的一個實踐,當然,除了AspectJ以外,還有很多其它的AOP實現,例如ASMDex,但目前最好、最方便的,依然是AspectJ。

在Android項目中使用AspectJ

AOP的用處非常廣,從Spring到Android,各個地方都有使用,特別是在后端,Spring中已經使用的非常方便了,而且功能非常強大,但是在Android中,AspectJ的實現是略閹割的版本,并不是所有功能都支持,但對于一般的客戶端開發來說,已經完全足夠用了。

在Android上集成AspectJ實際上是比較復雜的,不是一句話就能compile,但是,鄙司已經給大家把這個問題解決了,大家現在直接使用這個SDK就可以很方便的在Android Studio中使用AspectJ了。Github地址如下:

https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx

另外一個比較成功的使用AOP的庫是Jake大神的Hugo:

https://github.com/JakeWharton/hugo

接入說明

首先,需要在項目根目錄的build.gradle中增加依賴:

classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'

完整代碼如下:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.0-beta2'
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

然后再主項目或者庫的build.gradle中增加AspectJ的依賴:

compile 'org.aspectj:aspectjrt:1.8.9'

同時在build.gradle中加入AspectJX模塊:

apply plugin: 'android-aspectjx'

這樣就把整個Android Studio中的AspectJ的環境配置完畢了,如果在編譯的時候,遇到一些『can't determine superclass of missing type xxxxx』這樣的錯誤,請參考項目README中關于excludeJarFilter的使用。

aspectjx {
    //includes the libs that you want to weave
    includeJarFilter 'universal-image-loader', 'AspectJX-Demo/library'

    //excludes the libs that you don't want to weave
    excludeJarFilter 'universal-image-loader'
}

AspectJ入門

我們通過一段簡單的代碼來了解下基本的使用方法和功能,新建一個AspectTest類文件,代碼如下:

@Aspect
public class AspectTest {

    private static final String TAG = "xuyisheng";

    @Before("execution(* android.app.Activity.on**(..))")
    public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
        String key = joinPoint.getSignature().toString();
        Log.d(TAG, "onActivityMethodBefore: " + key);
    }
}

在類的最開始,我們使用@Aspect注解來定義這樣一個AspectJ文件,編譯器在編譯的時候,就會自動去解析,并不需要主動去調用AspectJ類里面的代碼。

我的原始代碼很簡單:

public class MainActivity extends AppCompatActivity {

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

通過這種方式編譯后,我們來看下生成的代碼是怎樣的。AspectJ的原理實際上是在編譯的時候,根據一定的規則解析,然后插入一些代碼,通過aspectjx生成的代碼,會在Build目錄下:

通過反編譯工具查看下生成內容:

我們可以發現,在onCreate的最前面,插入了一行AspectJ的代碼。這個就是AspectJ的主要功能,拋開AOP的思想來說,我們想做的,實際上就是『在不侵入原有代碼的基礎上,增加新的代碼』。

AspectJ之Join Points

Join Points,簡稱JPoints,是AspectJ的核心思想之一,它就像一把刀,把程序的整個執行過程切成了一段段不同的部分。例如,構造方法調用、調用方法、方法執行、異常等等,這些都是Join Points,實際上,也就是你想把新的代碼插在程序的哪個地方,是插在構造方法中,還是插在某個方法調用前,或者是插在某個方法中,這個地方就是Join Points,當然,不是所有地方都能給你插的,只有能插的地方,才叫Join Points。

AspectJ之Pointcuts

Join Points和Pointcuts的區別實際上很難說,我也不敢說我理解的一定對,但這些都是概念上的內容,并不影響我們去使用。

Pointcuts,在我理解,實際上就是在Join Points中通過一定條件選擇出我們所需要的Join Points,所以說,Pointcuts,也就是帶條件的Join Points,作為我們需要的代碼切入點。

AspectJ之Advice

又來一個Advice,Advice其實是最好理解的,也就是我們具體插入的代碼,以及如何插入這些代碼。我們最開始舉的那個例子,里面就是使用的最簡單的Advice——Before。類似的還有After、Around,我們后面來講講他們的區別。

AspectJ之切點語法

我們以前面的Demo來看下最簡單的AspectJ語法:

@Before("execution(* android.app.Activity.on**(..))")
public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
}

這里會分成幾個部分,我們依次來看:

  • @Before:Advice,也就是具體的插入點
  • execution:處理Join Point的類型,例如call、execution
  • (* android.app.Activity.on**(..)):這個是最重要的表達式,第一個『*』表示返回值,『*』表示返回值為任意類型,后面這個就是典型的包名路徑,其中可以包含『*』來進行通配,幾個『*』沒區別。同時,這里可以通過『&&、||、!』來進行條件組合。()代表這個方法的參數,你可以指定類型,例如android.os.Bundle,或者(..)這樣來代表任意類型、任意個數的參數。
  • public void onActivityMethodBefore:實際切入的代碼。

這里還有一些匹配規則,可以作為示例來進行講解:

表達式 含義
java.lang.String 匹配String類型
java.*.String 匹配java包下的任何“一級子包”下的String類型,如匹配java.lang.String,但不匹配java.lang.ss.String
java..* 匹配java包及任何子包下的任何類型,如匹配java.lang.String、java.lang.annotation.Annotation
java.lang.*ing 匹配任何java.lang包下的以ing結尾的類型
java.lang.Number+ 匹配java.lang包下的任何Number的自類型,如匹配java.lang.Integer,也匹配java.math.BigInteger
參數 含義
() 表示方法沒有任何參數
(..) 表示匹配接受任意個參數的方法
(..,java.lang.String) 表示匹配接受java.lang.String類型的參數結束,且其前邊可以接受有任意個參數的方法
(java.lang.String,..) 表示匹配接受java.lang.String類型的參數開始,且其后邊可以接受任意個參數的方法
(*,java.lang.String) 表示匹配接受java.lang.String類型的參數結束,且其前邊接受有一個任意類型參數的方法

AspectJ實例

Before、After

這兩個Advice應該是使用的最多的,所以,我們先來看下這兩個Advice的實例,首先看下Before和After。

@Before("execution(* com.xys.aspectjxdemo.MainActivity.on*(android.os.Bundle))")
public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
    String key = joinPoint.getSignature().toString();
    Log.d(TAG, "onActivityMethodBefore: " + key);
}

@After("execution(* com.xys.aspectjxdemo.MainActivity.on*(android.os.Bundle))")
public void onActivityMethodAfter(JoinPoint joinPoint) throws Throwable {
    String key = joinPoint.getSignature().toString();
    Log.d(TAG, "onActivityMethodAfter: " + key);
}

經過上面的語法解釋,現在看這個應該很好理解了,我們來看下編譯后的類:

我們可以看見,在原始代碼的基礎上,增加了Before和After的代碼,Log也能被正確的插入并打印出來。

Around

Before和After其實還是很好理解的,也就是在Pointcuts之前和之后,插入代碼,那么Around呢,從字面含義上來講,也就是在方法前后各插入代碼,是的,他包含了Before和After的全部功能,代碼如下:

@Around("execution(* com.xys.aspectjxdemo.MainActivity.testAOP())")
public void onActivityMethodAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    String key = proceedingJoinPoint.getSignature().toString();
    Log.d(TAG, "onActivityMethodAroundFirst: " + key);
    proceedingJoinPoint.proceed();
    Log.d(TAG, "onActivityMethodAroundSecond: " + key);
}

其中,proceedingJoinPoint.proceed()代表執行原始的方法,在這之前、之后,都可以進行各種邏輯處理。

原始代碼:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        testAOP();
    }

    public void testAOP() {
        Log.d("xuyisheng", "testAOP");
    }
}

我們先來看下編譯后的代碼:

我們可以發現,Around確實實現了Before和After的功能,但是要注意的是,Around和After是不能同時作用在同一個方法上的,會產生重復切入的問題。

自定義Pointcuts

自定義Pointcuts可以讓我們更加精確的切入一個或多個指定的切入點。

首先,我們需要自定義一個注解類,例如——DebugTool.java:

/**
 * 自定義AOP注解
 * <p>
 * Created by xuyisheng on 17/1/12.
 */

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface DebugTool {
}

然后在需要插入代碼的地方使用這個注解:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        testAOP();
    }

    @DebugTool
    public void testAOP() {
        Log.d("xuyisheng", "testAOP");
    }
}

最后,我們來創建自己的切入文件。

@Pointcut("execution(@com.xys.aspectjxdemo.DebugTool * *(..))")
public void DebugToolMethod() {
}

@Before("DebugToolMethod()")
public void onDebugToolMethodBefore(JoinPoint joinPoint) throws Throwable {
    String key = joinPoint.getSignature().toString();
    Log.d(TAG, "onDebugToolMethodBefore: " + key);
}

先定義Pointcut,并申明要監控的方法名,最后,在Before或者其它Advice里面添加切入代碼,即可完成切入。

編譯好的代碼如下:

通過這種方式,我們可以非常方便的監控指定的Pointcut,從而增加監控的粒度。

call和execution

在AspectJ的切入點表達式中,我們前面都是使用的execution,實際上,還有一種類型——call,那么這兩種語法有什么區別呢,我們來試驗下就知道了。

被切代碼依然很簡單:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        testAOP();
    }

    public void testAOP() {
        Log.d("xuyisheng", "testAOP");
    }
}

先來看execution,代碼如下:

@Before("execution(* com.xys.aspectjxdemo.MainActivity.testAOP(..))")
public void methodAOPTest(JoinPoint joinPoint) throws Throwable {
    String key = joinPoint.getSignature().toString();
    Log.d(TAG, "methodAOPTest: " + key);
}

編譯之后的代碼如下所示:

再來看下call,代碼如下:

@Before("call(* com.xys.aspectjxdemo.MainActivity.testAOP(..))")
public void methodAOPTest(JoinPoint joinPoint) throws Throwable {
    String key = joinPoint.getSignature().toString();
    Log.d(TAG, "methodAOPTest: " + key);
}

編譯之后的代碼如下所示:

其實對照起來看就一目了然了,execution是在被切入的方法中,call是在調用被切入的方法前或者后。

對于Call來說:

Call(Before)
Pointcut{
    Pointcut Method
}
Call(After)

對于Execution來說:

Pointcut{
  execution(Before)
    Pointcut Method
  execution(After)
}

切入點過濾與withincode

除了前面提到的call和execution,比較常用的還有一個withincode。這個語法通常來進行一些切入點條件的過濾,作更加精確的切入控制。我們可以參考下面這個例子:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        testAOP1();
        testAOP2();
    }

    public void testAOP() {
        Log.d("xuyisheng", "testAOP");
    }

    public void testAOP1() {
        testAOP();
    }

    public void testAOP2() {
        testAOP();
    }
}

testAOP1()和testAOP2()都調用了testAOP()方法,但是,現在想在testAOP2()方法調用testAOP()方法的時候,才切入代碼,那么這個時候,就需要使用到Pointcut和withincode組合的方式,來精確定位切入點。

// 在testAOP2()方法內
@Pointcut("withincode(* com.xys.aspectjxdemo.MainActivity.testAOP2(..))")
public void invokeAOP2() {
}

// 調用testAOP()方法的時候
@Pointcut("call(* com.xys.aspectjxdemo.MainActivity.testAOP(..))")
public void invokeAOP() {
}

// 同時滿足前面的條件,即在testAOP2()方法內調用testAOP()方法的時候才切入
@Pointcut("invokeAOP() && invokeAOP2()")
public void invokeAOPOnlyInAOP2() {
}

@Before("invokeAOPOnlyInAOP2()")
public void beforeInvokeAOPOnlyInAOP2(JoinPoint joinPoint) {
    String key = joinPoint.getSignature().toString();
    Log.d(TAG, "onDebugToolMethodBefore: " + key);
}

我們再來看下編譯后的代碼:

我們可以看見,只有在testAOP2()方法中被插入了代碼,這就做到了精確條件的插入。

異常處理AfterThrowing

AfterThrowing是一個比較少見的Advice,他用于處理程序中未處理的異常,記住,這點很重要,是未處理的異常,具體原因,我們等會看反編譯出來的代碼就知道了。我們隨手寫一個異常,代碼如下:

public void testAOP() {
    View view = null;
    view.animate();
}

然后使用AfterThrowing來進行AOP代碼的編寫:

@AfterThrowing(pointcut = "execution(* com.xys.aspectjxdemo.*.*(..))", throwing = "exception")
public void catchExceptionMethod(Exception exception) {
    String message = exception.toString();
    Log.d(TAG, "catchExceptionMethod: " + message);
}

這段代碼很簡單,同樣是使用我們前面類似的表達式,但是這里是為了處理異常,所以,使用了*.*來進行通配,在異常中,我們執行一行日志,編譯好的代碼如下:

我們可以看見com.xys.aspectjxdemo包下的所有方法都被加上了try catch,同時,在catch中,被插入了我們切入的代碼,但是最后,他依然會throw e,也就是說,這個異常已經會被拋出去,崩潰依舊是會發生的。同時,如果你的原始代碼中已經try catch了,那么同樣也無法處理,具體原因,我們看一個反編譯的代碼:

可以看見,實際上,原始代碼的catch中,又被套了一層try catch,所以,e.printStackTrace()被try catch,也就不會再有異常發生了,也就無法切入了。

AspectJX使用案例

目前鄙司的很多項目都已經使用了這套AOP方案,例如基于AOP的動態權限管理、基于AOP的業務數據埋點、基于AOP的性能監測系統等等。

現在已經開源了一部分基于AOP的動態權限管理的源碼,但由于需要剝離業務代碼,所以后面會更加完善這功能代碼,大家可以繼續關注,github地址如下所示:

https://github.com/firefly1126/android_permission_aspectjx

其它的AOP項目陸續開源中,大家可以持續關注~

歡迎關注我的微信公眾號:Android群英傳

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

推薦閱讀更多精彩內容

  • 因為工作需求,自己去了解一下aop并做下的記錄,當然大部分都是參考他人博客以及官方文檔。 目錄 [關于 AOP](...
    forip閱讀 2,284評論 1 20
  • What? As we all know,在進行項目構建時,追求各模塊高內聚,模塊間低耦合。然而現實并不總是如此美...
    MasterNeo閱讀 2,092評論 0 17
  • 團隊開發框架實戰—面向切面的編程 AOP 引言 軟件開發的目標是要對世界的部分元素或者信息流建立模型,實現軟件系統...
    Bobby0322閱讀 4,168評論 4 49
  • AOP之AspectJ 前言:這幾天一直在學習aop切面編程,以前一直也有聽過aop但是實際用的還是比較少,不是很...
    六_六閱讀 1,776評論 0 0
  • 2016年9月21日 今天和彩麗幾個媽媽做個一個小型的家庭聚會,產品試驗,化妝,促進感情,以美的視角建立一個媽媽圈...
    徐曉美閱讀 247評論 0 0