性能優化,還得看AspectJ

/   AOP   /

AOP:面向切面編程(Aspect-Oriented Programming)。java

若是說,OOP若是是把問題劃分到單個模塊的話,那麼AOP就是把涉及到衆多模塊的某一類問題進行統一管理。好比有三個模塊:登錄、轉帳和大文件上傳,如今須要加入性能檢測功能,統計這三個模塊每一個方法耗時多少。android

OOP思想作法是設計一個性能檢測模塊,提供接口供這三個模塊調用。這樣每一個模塊都要調用性能檢測模塊的接口,若是接口有改動,須要在這三個模塊中每次調用的地方修改。面試

AOP的思想作法是:在這些獨立的模塊間,在特定的切入點進行hook,將共同的邏輯添加到模塊中而不影響原有模塊的獨立性。編程

因此這就是上面所說的:把涉及到衆多模塊的某一類問題進行統一管理。緩存

安卓AOP三劍客:APT,AspectJ和Javassist。服務器

  • APT應用:Dagger,butterKnife,組件化方案等等網絡

  • AspectJ:主要用於性能監控,日誌埋點等架構

  • Javassist:熱更新(能夠在編譯後,打包Dex以前幹事情,能夠突破一下限制)app

/   AspectJ   /

今天的主角是AspectJ,主要用於不想侵入原有代碼的場景中,例如SDK須要無侵入的在宿主中插入一些代碼,作日誌埋點、性能監控、動態權限控制、甚至是代碼調試等等。ide

接入說明

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

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

複製代碼

而後再主項目或者庫的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'
} 

複製代碼
基本使用

直接看代碼,Activity中:

@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        testMethod();
    }

    private void testMethod() {
        Log.e(DemoAspect.TAG, "testMethod-invoke");
    } 

複製代碼

新建DemoAspect類:

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

    @Before("execution(* com.hujiang.library.demo.DemoActivity.test*(..))")
    public void testAspectBefore(JoinPoint point) {
        Log.e(TAG, point.getSignature().getName()+"-before ");
    }
} 

複製代碼

運行時候打印:

testMethod-before com.hujiang.library.demo E/DemoAspect: testMethod-invoke 

複製代碼

因此完成插入操做咱們只須要

  1. 類上加入註釋@Aspect

  2. 方法上加入註釋@Before

  3. Before裏寫入要插入的相關信息

是否是很簡單,下面就一一詳細講解。

Advice

就是說咱們要插入的代碼以何種方式插入,就是方法上的註釋。

Before和After很好理解,上面的例子已經展現的很清楚了。

AfterReturning

適用於須要獲取到返回值的狀況好比:

private int AfterReturningTest() {
    Log.e(DemoAspect.TAG, "AfterReturning-invoke");
    return 10;
}

@AfterReturning(value = "execution(* com.hujiang.library.demo.DemoActivity.AfterReturning*(..))", returning = "num")
public void testAspectAfterReturning(int num) {
    Log.e(TAG, "AfterReturning-num:" + num);
} 

複製代碼

這樣就能夠在切面方法裏獲取到返回值了,值得注意的是方法參數必須和註解中的值一致。

【returning = 「num」】===【int num】 

複製代碼

AfterThrowing

適用於收集和監控異常信息。

private void AfterThrowingTest() {
    View v = null;
    v.setVisibility(View.VISIBLE);
}

@AfterThrowing(value = "execution(* com.hujiang.library.demo.DemoActivity.AfterThrowing*(..))", throwing = "exception")
public void testAspectAfterReturning(Exception exception) {
    Log.e(TAG, "AfterThrowing-exception:" + exception.getMessage());
} 

複製代碼

一樣,參數和註解裏的值必須一致。這裏崩潰一樣會發生,不會由於切面操做而直接catch住,只是在拋出異常以前會打印出異常信息而已。

Around

在方法執行先後均可調用,比較靈活。

private void AroundTest() {
    Log.e(DemoAspect.TAG, "AroundTest-invoke");
}

@Around("execution(* com.hujiang.library.demo.DemoActivity.AroundTest(..))")
public void testAspectAround(ProceedingJoinPoint point) throws Throwable {
    Log.e(TAG, point.getSignature().getName() + "-before ");
    point.proceed();
    Log.e(TAG, point.getSignature().getName() + "-after ");
} 

複製代碼

經過執行ProceedingJoinPoint的proceed方法調用原方法,靈活控制。若是你想的話也能夠不調用,直接攔截了。

Pointcut

告訴代碼注入工具,在何處注入一段特定代碼的表達式。也就是例子中的這句話:

@Before("execution(* com.hujiang.library.demo.DemoActivity.test*(..))") 

複製代碼

咱們分紅幾個部分依次來看:

@Before:Advice,也就是具體的插入點,咱們已經介紹過

execution:處理Join Point的類型,例如call、execution、withincode

其中call、execution相似,都是插入代碼的意思,區別就是execution是在被切入的方法中,call是在調用被切入的方法前或者後。

//對於Call來講:
Call(Before)
Pointcut{
    Pointcut Method
}
Call(After)

//對於Execution來講:
Pointcut{
  execution(Before)
    Pointcut Method
  execution(After)
} 

複製代碼

withcode這個語法一般來進行一些切入點條件的過濾,做更加精確的切入控制,好比:

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

    public void test() {
        Log.e("qiuyunfei""test");
    }

    public void test1() {
        test();
    }

    public void test2() {
        test();
    } 

複製代碼

假如咱們想要切test方法,可是隻但願在test2中調用test才執行切面方法,就須要用到withincode。

// 在test()方法內
@Pointcut("withincode(* com.hujiang.library.aspect.MainActivity.test2(..))")
public void invoke2() {
}

// 調用test()方法的時候
@Pointcut("call(* com.hujiang.library.aspect.MainActivity.test(..))")
public void invoke() {
}

// 同時知足前面的條件,即在test2()方法內調用test()方法的時候才切入
@Pointcut("invoke() && invoke2()")
public void invokeOnlyIn2() {
}

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

複製代碼

MethodPattern:這個是最重要的表達式,大體爲:@註解和訪問權限,返回值的類型和包名.函數名(參數)

@註解和訪問權限(public/private/protect,以及static/final):屬於可選項。若是不設置它們,則默認都會選擇。以訪問權限爲例,若是沒有設置訪問權限做爲條件,那麼public,private,protect及static、final的函數都會進行搜索。

返回值類型:就是普通的函數的返回值類型。若是不限定類型的話,就用*通配符表示。

包名.函數名:用於查找匹配的函數。可使用通配符,包括和…以及+號。其中號用於匹配除.號以外的任意字符,而…則表示任意子package,+號表示子類。

* com.hujiang.library.demo.DemoActivity.test*(..) 

複製代碼

第一部分:『』表示返回值,『』表示返回值爲任意類型。

第二部分:就是典型的包名路徑,其中能夠包含『』來進行通配,幾個『』沒區別。同時,這裏能夠經過『&&、||、!』來進行條件組合。相似【test*】的寫法,表示以test開頭爲方法名的任意方法。

第三部分:()表明這個方法的參數,你能夠指定類型,例如android.os.Bundle,或者(…)這樣來表明任意類型、任意個數的參數,也能夠混合寫法(android.os.Bundle,…)這樣來表明第一個參數爲bundle,後面隨意。

自定義Pointcuts:有時候咱們須要指定哪些方法須要進行AOP操做,目標明確,也能夠經過註解的方式來完成。首先聲明註解:

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface AspectAnnotation {
} 

複製代碼

而後在切面類中定義:

//定義一個使用該註解的Pointcut
@Pointcut("execution(@com.hujiang.library.aspect.AspectAnnotation * *(..))")
public void AspectAnnotation(){

}

@Before("AspectAnnotation()")
public void testAspectAnnotation(JoinPoint point){
    Log.e(TAG, point.getSignature().getName() + "-Before ");
}

//在Activity中使用
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    AnnotationTest();
}
@AspectAnnotation
private void AnnotationTest() {
    Log.e(DemoAspect.TAG, "AnnotationTest-invoke");
} 

複製代碼

使用很簡單,前面也介紹過MethodPattern,註釋@在這裏就不能省略了。

/   AspectJ實戰   /

實現登陸檢查的操做

不少app都有這個需求,在操做前提醒用戶註冊登陸,跳轉到註冊或者登陸界面,若是用AspectJ實現就顯得很是簡潔且無侵入性。

private static final String TAG = "AspectCommonTool";

@Pointcut("execution(@xxx.aspectj.annotation.NeedLogin * *(..))")
public void needLoginMethod() {
}

/**  * 在@NeedLogin方法中插入  * 若在非Activity中使用@NeedLogin,參數必須傳入Context做爲跳轉起始頁  */
@Around("needLoginMethod()")
public void onNeedLoginAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
    Context mContext = null;
    //proceedingJoinPoint.getThis()能夠獲取到調用該方法的對象
    if (proceedingJoinPoint.getThis() instanceof Context) {
        mContext = (Context) proceedingJoinPoint.getThis();
    } else {
        //proceedingJoinPoint.getArgs()能夠獲取到方法的全部參數
        for (Object context : proceedingJoinPoint.getArgs()) {
            if (context instanceof Context) {
                mContext = (Context) context;
                break;
            }
        }
    }
    if (mContext == null) {
        return;
    }
    if (LoginUtils.isLogin()) {
        /**          * 若是用戶登陸則執行原方法          */
        proceedingJoinPoint.proceed();
    } else {
        /**          * 未登陸狀況下跳轉至登陸註冊主界面          */
    } 

複製代碼

使用很方便,毫無侵入性,後期也很好維護。相似的思想也能夠實現:檢查網絡情況、檢查權限狀態、避免按鈕屢次點擊、自動完成緩存等狀況。

性能監控

AspectJ其實在Android中的應用主要仍是性能監控、日誌埋點等,下面以一個簡單的例子表示:

咱們監控佈局加載耗時,判斷佈局是否嵌套過多或者過於複製致使Activity啓動卡頓,首先咱們知道Activity是經過setContentView方法加載佈局的:

  1. 佈局解析過程,IO過程

  2. 建立View爲反射過程

這兩步均爲耗時操做,因此咱們須要監控setContentView。

@Around("execution(* android.app.Activity.setContentView(..))")
public void getContentViewTime(ProceedingJoinPoint point) throws Throwable {
    String name = point.getSignature().toShortString();
    long time = System.currentTimeMillis();
    point.proceed();
    Log.e(TAG, name + " cost: " + (System.currentTimeMillis() - time));
}

//Activity
public class DemoActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
    }
} 

複製代碼

打印日誌以下:

DemoAspect: AppCompatActivity.setContentView(..) cost: 76 

複製代碼

/   總結   /

平常開發中,咱們能夠將耗時上傳到服務器,收集用戶信息,找到卡頓Activity作出對應優化。

固然,這只是很是簡單的實現,實際開發中還能夠監控各類你想監控的位置。 Android學習PDF+架構視頻+面試文檔+源碼筆記

相關文章
相關標籤/搜索