Android中的AOP的實現及AspectJ的使用

1、OOP和AOP的簡單簡介和區別

OOP(Object Oriented Programming): 這就是咱們android中的面向對象開發。面向對象的三大特徵,封裝、繼承和多態。這裏很少贅述。java

AOP(Aspect Oriented Programming):面向切面編程;AOP則是面對業務邏輯處理過程當中的切面進行提取,也就是程序處理的某個步驟或者階段,以達到代碼間的低耦合、代碼分離、提升代碼重用性android


2、Android中AOP的實現

2.一、Java Annotation的簡介

在咱們andorid開發中都使用過註解功能,第三方庫有註解的有ButterKnif、dagger二、EventBus、Retrofit,其實這些庫部分核心功能也是基於AOP實現的。只不過他們還用到了其餘插件,好比APT,APT在程序編譯期,掃描代碼中的註解信息,併爲咱們生成java代碼,實現咱們的功能,無需咱們手動去處理。面試

Java Annotation是JDK5.0引入的註解機制。在咱們代碼裏。常常能夠看到@Override:表示方法覆蓋父類方法。編程

java中的Annotation:

@Deprecated  --  所標註內容,再也不被建議使用。
@Override    --  只能標註方法,表示該方法覆蓋父類中的方法。
@Documented  --  所標註內容,能夠出如今javadoc中。
@Inherited   --  只能被用來標註「Annotation類型」,它所標註的Annotation具備繼承性。
@Retention   --  只能被用來標註「Annotation類型」,並且它被用來指定Annotation的RetentionPolicy屬性。
@Target      --  只能被用來標註「Annotation類型」,並且它被用來指定Annotation的ElementType屬性。
@SuppressWarnings --  所標註內容產生的警告,編譯器會對這些警告保持靜默。
複製代碼

2.二、自定義Annotation加反射實現findViewById功能

自定義Annotation,實現本身的註解markdown

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInject {
    int value();
}
複製代碼

MyInject的反射處理工具
public class MyInjectUtils {
    public static void injectViews(Activity activity) {
        Class<? extends Activity> object = activity.getClass(); // 獲取activity的Class
        Field[] fields = object.getDeclaredFields(); // 經過Class獲取activity的全部字段
        for (Field field : fields) { // 遍歷全部字段
            // 獲取字段的註解,若是沒有ViewInject註解,則返回null
            MyInject viewInject = field.getAnnotation(MyInject.class);
            if (viewInject != null) {
                int viewId = viewInject.value(); // 獲取字段註解的參數,這就是咱們傳進去控件Id
                if (viewId != -1) {
                    try {
                        // 獲取類中的findViewById方法,參數爲int
                        Method method = object.getMethod("findViewById", int.class);
                        // 執行該方法,返回一個Object類型的View實例
                        Object resView = method.invoke(activity, viewId);
                        field.setAccessible(true);
                        // 把字段的值設置爲該View的實例
                        field.set(activity, resView);
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
複製代碼

在Activity裏的使用
@MyInject(R.id.button)
    Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyInjectUtils.injectViews(this);
    }
複製代碼

這樣咱們就實現了findViewById的功能了。不難發現,此功能和ButterKnif的findViewById很是類似,可是有本質的區別。由於咱們採用了反射,在android中是很是消耗性能的。因此那些第三方庫則會採起Annotation+APT來作,把註解譯成Java代碼,避免性能損耗。可是你知道了這些,面試官繼續問你這些註解第三方庫的原理,也不至於啞口無言!!app

3、AspectJ的使用及使用場景等(重點)

AspectJ:是一個代碼生成工具,AspectJ語法就是用來定義代碼生成規則的語法ide

3.一、項目中引用

項目build.gradle中:函數

dependencies {
        //...
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.8'
    }
複製代碼

app裏的build.gradle中頂部添加工具

apply plugin: 'android-aspectjx'
複製代碼

3.二、使用場景:數據埋點

一般咱們數據埋點都會經過Application中的registerActivityLifecycleCallbacks監聽。但這裏咱們使用AspectJ。代碼以下(這裏的標註@Before、@After,關鍵字execution,call後面詳細講解,這裏咱們先把功能實現了):oop

//標註咱們要經過Aspect語法生成代碼的輔助類
@Aspect
public class AspectHelper {
    private final String TAG = this.getClass().getSimpleName();
    
    //com.lihang.aoptestpro.BaseActivity 是我項目裏的BaseActivity
    //這句代碼實現的功能是:會打印咱們項目裏全部Activity裏全部的on開頭的方法
    //joinPoint.getThis().getClass().getSimpleName() 當前Activity的類名
    @Before("execution(* com.lihang.aoptestpro.BaseActivity.on**(..))")
    public void onActivityStart(JoinPoint joinPoint) throws Throwable {
        String key = joinPoint.getSignature().toString();
        Log.i(TAG, key + "============" + joinPoint.getThis().getClass().getSimpleName());
    }

    //會打印咱們項目裏全部Activity裏的onPause方法。
    @Before("execution(* com.lihang.aoptestpro.BaseActivity.onPause(..))")
    public void onActivityPause(JoinPoint joinPoint) throws Throwable {
        String key = joinPoint.getSignature().toString();
        Log.i(TAG, key + "============" + joinPoint.getThis().getClass().getSimpleName());
    }
}
複製代碼

至此,拿到全部Activity的生命週期,埋點功能就能夠實現了;注意及總結:

  • 切忌勿用
@Before("execution(* android.app.Activity.on**(..))")
複製代碼

網上大部分都是用這句,實踐發現會除了會走咱們的Activity還會走系統的Activity及FragmentActivity.至少3次


  • 使用BaseActivity
@Before("execution(* com.lihang.aoptestpro.BaseActivity.on**(..))")
複製代碼

若是你BaseActivity不去實現系統生命週期,你會發現根本不走。因此好比要抓onStart、onPause生命週期時,必定要在BaseActivity去實現,即便方法體內是空也行

其實這裏還能用下面的語法實現,前提是你全部的Activity必須以「Activity」字符串做爲類名的尾部

@Before("execution(* com.lihang.aoptestpro.*Activity.on**(..))")
複製代碼

3.三、使用場景:登陸校驗

咱們在項目開發時,有些功能每每須要登陸後才能使用,若是沒有登陸,就去跳轉登陸頁面。這樣就避免不了if/else的判斷。以下,點擊關注時的代碼

public void follow() {
        if (MyApplication.getInstance().getLoginUser() != null) {
            User user = MyApplication.getInstance().getLoginUser();
            Log.i(TAG, "已登陸,user不爲空,用user信息去實現關注");
        } else {
            Log.i(TAG, "未登陸,跳轉登陸頁面");
        }
    }
複製代碼

那麼使用AOP非侵入式怎麼使用呢? 首先咱們先定義個標註

@Target(ElementType.METHOD)//這裏是標註方法,以前那個Filed是標註屬性
@Retention(RetentionPolicy.RUNTIME)
public @interface IsLogin {
}
複製代碼

而後看咱們的Aspect裏:

@Aspect
public class AspectHelper {
    private final String TAG = this.getClass().getSimpleName();
    
    @Around("execution(@com.lihang.aoptestpro.IsLogin * *(..))")
    public void isLoginOn(ProceedingJoinPoint joinPoint) throws Throwable {
        if (MyApplication.getInstance().getLoginUser() != null) {
            //joinPoint.proceed()能夠當作就是咱們用@IsLogin標註的那個方法,調用proceed意思就是繼續執行方法
            //這裏的意思就是全部用標註@IsLogin標註的,是登陸狀態纔會繼續執行方法,不然會執行咱們下面的去登陸,不會執行原方法
            joinPoint.proceed();
        } else {
            Log.i(TAG, "user爲空,快去登陸把!!");
        }
    }
}
複製代碼

而後再看看咱們的follow方法。用@IsLogin標註後,就能夠直接處理登陸狀態就好了。真的是低耦合,代碼複用性高

@IsLogin
    public void follow() {
        User user = MyApplication.getInstance().getLoginUser();
        Log.i(TAG, "已登陸,user不爲空,用user信息去實現關注");
    }
複製代碼

4、AspectJ常見關鍵字以及各自的區別

4.一、常見標註介紹

  • @Before:意思就是在方法以前執行
//意思是onActivityPause會在BaseActivity.onPause()方法前執行
    @Before("execution(* com.lihang.aoptestpro.BaseActivity.onPause(..))")
    public void onActivityPause(JoinPoint joinPoint) throws Throwable {

    }
複製代碼
  • @After:同理則是在方法後執行
  • @Around:包括了@Befor和@After的功能,其能夠進行控制
//joinPoint.proceed()是控制方法是否繼續往下執行
    //在joinPoint.proceed()前的邏輯代碼,就是實現@Before的功能,在方法前執行
    //在joinPoint.proceed()後的邏輯代碼,就是實現@After的功能,在方法後執行
    @Around("execution(@com.lihang.aoptestpro.IsLogin * *(..))")
    public void isLoginOn(ProceedingJoinPoint joinPoint) throws Throwable {
        if (MyApplication.getInstance().getLoginUser() != null) {
            joinPoint.proceed();
        } else {
            Log.i("MainActivity", "user爲空,快去登陸把!!");
        }
    }
複製代碼

注意點:

  1. 當Action爲Before、After時,方法入參爲JoinPoint。
  2. 當Action爲Around時,方法入參爲ProceedingPoint。
  3. Around和Before、After的最大區別: ProceedingPoint不一樣於JoinPoint,其提供了proceed方法執行目標方法。

4.2常見關鍵字介紹

翻閱了大量資料,一樣的代碼。從其生成的代碼裏看。

  • call:插入在函數體體外

簡單看就是:

Call(Before)
Pointcut{
   Pointcut Method
}
Call(After)
複製代碼

  • execution:插入在函數體內

簡單看就是:

Pointcut{
  execution(Before)
    Pointcut Method
  execution(After)
}
複製代碼

雖然知道其工做原理了。但做者也存在一個疑問,那就是什麼call和execution都能實現同一的功能。可是什麼場景使用哪一個更佳呢?但願有知道的小夥伴幫忙解答下

參考文獻

看 AspectJ 在 Android 中的強勢插入
大話AOP與Android的愛恨情仇
Android 自動化埋點:基於AspectJ的滬江SDK的使用整理

個人公衆號

本人最近也在開始準備面試。費曼學習法,從本身弄明白開始,用淺白的語言敘述。寫博客也是這個目的吧。在準備面試資料的同事遇到新知識點,也要各個擊破、喜歡的話,能夠關注下公衆號

相關文章
相關標籤/搜索