AOP:面向切面編程(Aspect-Oriented Programming)。java
若是說,OOP若是是把問題劃分到單個模塊的話,那麼AOP就是把涉及到衆多模塊的某一類問題進行統一管理。好比有三個模塊:登錄、轉帳和大文件上傳,如今須要加入性能檢測功能,統計這三個模塊每一個方法耗時多少。android
OOP思想作法是設計一個性能檢測模塊,提供接口供這三個模塊調用。這樣每一個模塊都要調用性能檢測模塊的接口,若是接口有改動,須要在這三個模塊中每次調用的地方修改。面試
AOP的思想作法是:在這些獨立的模塊間,在特定的切入點進行hook,將共同的邏輯添加到模塊中而不影響原有模塊的獨立性。編程
因此這就是上面所說的:把涉及到衆多模塊的某一類問題進行統一管理。緩存
安卓AOP三劍客:APT,AspectJ和Javassist。服務器
APT應用:Dagger,butterKnife,組件化方案等等網絡
AspectJ:主要用於性能監控,日誌埋點等架構
Javassist:熱更新(能夠在編譯後,打包Dex以前幹事情,能夠突破一下限制)app
今天的主角是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
複製代碼
因此完成插入操做咱們只須要
類上加入註釋@Aspect
方法上加入註釋@Before
Before裏寫入要插入的相關信息
是否是很簡單,下面就一一詳細講解。
就是說咱們要插入的代碼以何種方式插入,就是方法上的註釋。
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,註釋@在這裏就不能省略了。
實現登陸檢查的操做
不少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方法加載佈局的:
佈局解析過程,IO過程
建立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+架構視頻+面試文檔+源碼筆記