Android AOP編程思想和實例(附帶源碼)

AOP 介紹

OOP爲 Object Oriented Programming,面向對象編程 把功能或問題模塊化,每個模塊處理自己的家務事。
AOP爲 Aspect Oriented Programming 的縮寫,意爲:面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。

  • AOP的特點:

1、Aspect Oriented Programming 面向切面編程 通過預編譯方式和運行期動態代理實現程序功能的統一維護
2、在運行時,編譯時,類加載期,動態地將代碼切入到類的指定方法、指定位置上的編程思想。
3、AOP在編程歷史上可以說是里程碑式的,對OOP編程是一種十分有益的補充。
4、AOP像OOP一樣,只是一種編程方法論,AOP並沒有規定說,實現AOP協議的代碼,要用什麼方式去實現。
5、OOP側重靜態,名詞,狀態,組織,數據,載體是空間;
6、AOP側重動態,動詞,行爲,調用,算法,載體是時間;

  • 我們通過 AspectJ 來實現AOP編程的設計。

介紹:AspectJ是一個面向切面編程的框架。AspectJ是對java的擴展,而且是完全兼容java的,AspectJ定義了AOP語法,它有一個專門的編譯器用來生成遵守Java字節編碼規範的Class文件。AspectJ還支持原生的Java,只需要加上AspectJ提供的註解即可。在Android開發中,一般就用它提供的註解和一些簡單的語法就可以實現絕大部分功能上的需求了。

  • AOP 中的術語
AOP術語 解釋
Joinpoint(連接點) 所謂連接點是指那些被攔截到的點
Pointcut(切入點) 所謂切入點是指我們要對哪些 Joinpoint 進行攔截的定義
Advice(通知/增強) 所謂通知是指攔截到 Joinpoint 之後所要做的事情就是通知
Introduction(引介) 引介是一種特殊的通知在不修改類代碼的前提下, Introduction 可以在運行期爲類 動態地添加一些方法或 Field
Target(目標對象) 代理的目標對象
Weaving(織入) 是指把增強應用到目標對象來創建新的代理對象的過程. AspectJ 採用編譯期織入和類裝在期織入
Proxy(代理) 一個類被 AOP 織入增強後,就產生一個結果代理類
Aspect(切面) 是切入點和通知(引介)的結合
  • Advice分類
Advice分類 解釋
Before 前置通知, 在目標執行之前執行通知
After 後置通知, 目標執行後執行通知
Around 環繞通知, 在目標執行中執行通知, 控制目標執行時機
AfterReturning 後置返回通知, 目標返回時執行通知
AfterThrowing 異常通知, 目標拋出異常時執行通知
  • 切入點指示符
切入點指示符 解釋
execution 用於匹配方法執行的連接點
within 用於匹配指定類型內的方法執行
this 用於匹配當前AOP代理對象類型的執行方法;注意是AOP代理對象的類型匹配,這樣就可能包括引入接口也類型匹配
target 用於匹配當前目標對象類型的執行方法;注意是目標對象的類型匹配,這樣就不包括引入接口也類型匹配
args 用於匹配當前執行的方法傳入的參數爲指定類型的執行方法
@within 用於匹配所以持有指定註解類型內的方法
@target 用於匹配當前目標對象類型的執行方法,其中目標對象持有指定的註解
@args 用於匹配當前執行的方法傳入的參數持有指定註解的執行
@annotation 用於匹配當前執行方法持有指定註解的方法

AOP實例

案例1:下面是通過 AOP 實現性能統計的實例。此例子爲統計不同方法模塊執行的時間統計

  • 首先項目引入 AspectJ
    添加依賴
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    implementation 'org.aspectj:aspectjrt:1.9.5'

}
//>>>>>>>>>>>>>>>>>>>Aspectjrt 在APP主工程下使用的配置>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.9.5'
    }
}

repositories {
    mavenCentral()
}

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.5",
                         "-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;
            }
        }
    }
}

  • 新建註解用來標記哪些方法需要統計
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BehaviorTrace {
    String value() default "";
}

  • 新建切面類,用來處理統計的邏輯
// 這個註解說明了此類爲切面
@Aspect
public class BehaviorTraceAspect {
	 //定義切面的規則
    //1、就再原來的應用中那些註解的地方放到當前切面進行處理
    //execution(註解名   註解用的地方) --- 固定寫法  *(..) 爲全部地方 的 全部註解
    @Pointcut("execution(@com.example.myapplication.ann.BehaviorTrace *  *(..))")
    public void BehaviorTraceAspectMethod(){

    }

    //2、對進入切面的內容如何處理
    //@Before 在切入點之前運行
	//@After("methodAnnottatedWithBehaviorTrace()")
    //@Around 在切入點前後都運行
    @Around("BehaviorTraceAspectMethod()")
    public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
    	// 獲取 MethodSignature 
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String className = methodSignature.getDeclaringType().getSimpleName();
        String methodName = methodSignature.getName();
        String value = methodSignature.getMethod().getAnnotation(BehaviorTrace.class).value();
        Log.d("weaveJoinPoint","方法執行前");
        long begin = System.currentTimeMillis();
        // 這個方法調用後執行標記註解的方法(連接點的執行),否則在 @Around時方法內部不會執行 
        Object result = joinPoint.proceed();
        long duration = System.currentTimeMillis() - begin;
        Log.d("weaveJoinPoint", String.format("%s功能:%s類的%s方法執行完成,用時%d ms",
                value, className, methodName, duration));
        return result;
    }
}

  • 使用方法,假設統計幾個按鈕點擊後方法的執行時間。
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

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

    }

    /**
     * 搖一搖
     */
    @BehaviorTrace("shark")
    public void shark(View view) {
        Log.d(TAG, "執行了shark方法");
        SystemClock.sleep(new Random().nextInt(2000));
    }

    /**
     * 掃一掃
     */
    @BehaviorTrace("scan")
    public void scan(View view) {
    }

    /**
     * 朋友圈
     */
    @BehaviorTrace("circleOfFriends")
    public void circleOfFriends(View view) {
    }

    /**
     * 小程序
     */
    @BehaviorTrace("miniApp")
    public void miniApp(View view) {
    }
}

  • 這就是aop編程簡單的實例,這種編程方式有很多可以用的地方,比如我們做事件統計,只需要在需要統計的點擊方法上添加自定義的註解,然後可以使用 @before 或者 @after 在方法調用前後者後去做統計操作。

只要再添加一個註解就可以同時統計時間和點擊統計了,解析的時候和上面基本一直,定義切面,定義規則等。

 /**
     * 搖一搖
     */
    @BehaviorTrace("shark")
    @ClickBehaviorTrace("統計點擊了搖一搖")
    public void shark(View view) {
        Log.d(TAG, "執行了shark方法");
        SystemClock.sleep(new Random().nextInt(2000));
    }


  • 實際上是框架在內部給生成了一些代碼。具體的在 對應的build下
    -app/build/intermediates/javac/debug/classes/com/example/myapplication/MainActivity.class
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    public MainActivity() {
    }

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        this.setContentView(2131361820);
    }

    @BehaviorTrace("shark")
    public void shark(View view) {
        JoinPoint var3 = Factory.makeJP(ajc$tjp_0, this, this, view);
        shark_aroundBody1$advice(this, view, var3, BehaviorTraceAspect.aspectOf(), (ProceedingJoinPoint)var3);
    }

    // 。。。。
}

案例2:一般應用都有點擊一些功能的時候未登錄的話跳轉登錄的需求,一般寫法就是

public void scan(View view) {
        if (isLogin){
            toDoSomething()
        }else{
            toLogin()
        }
}

下面用 aop 思想去實現。

實現方式1: 使用動態代理實現;

  • 新建一個 ILogin 接口
public interface ILogin {
    void login();
}
  • 新建一個代理Handler類

public class LoginInvocationHandler implements InvocationHandler {


    private Object target;
    private Context context;

    public LoginInvocationHandler( Context context) {
        this.target = context;
        this.context = context;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        // 判斷是否登錄的邏輯 我這裏簡單用全局靜態變量去記錄了一下  跳轉到登錄頁面 狀態改爲 true 默認爲false
        if (ContactValue.isLogin){
        	// 	登錄了執行代理實現的方法
            result = method.invoke(target,args);
        }else{
        	// 未登錄去登錄
            Intent intent = new Intent(context, LoginActivity.class);
            context.startActivity(intent);
        }
        return result;
    }
}

  • 調用方式
public class MainActivity extends AppCompatActivity implements ILogin {

    private static final String TAG = "MainActivity";
    private ILogin loginProxy;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        /**
         * 第一個參數:類加載器
         * 第二個參數:代理對象的目標類
         * 第三個參數:回調處理類
         */
        loginProxy = (ILogin) Proxy.newProxyInstance(getClassLoader(), new Class[]{ILogin.class}, new LoginInvocationHandler(this));
    }


    public void shark(View view) {
        loginProxy.login();
    }

    /**
     * 登錄後跳轉掃描功能
     */
    public void scan(View view) {
        loginProxy.login();
    }

    @Override
    public void login() {
        // 區分跳轉到哪裏  登錄後根據反射調用到這個方法 具體跳轉邏輯
        Intent intent = new Intent(this, SearchActivity.class);
        startActivity(intent);
    }
}

實現方式2 :採用註解的 aop 方式
原理同上面講的一樣這裏直接貼代碼了:

  • 註解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginFilter {
    int userDefine() default 0;
}
  • 切面
@Aspect
public class LoginAspect {
    @Pointcut("execution(@com.example.myapplication.ann.LoginFilterAnn * *(..))")
    public void loginAspect() {
    }

    @Around("loginAspect()")
    public void LoginAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Context context = LoginAssistant.getInstance().getContext();
        ILogin iLogin = LoginAssistant.getInstance().getiLogin();
        if (iLogin == null || context == null) {
            throw new Exception("LoginSDK 未進行初始化...");
        }
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        LoginFilterAnn loginFilterAnn = methodSignature.getMethod().getAnnotation(LoginFilterAnn.class);
        if (loginFilterAnn == null) {
            return;
        }
        if (iLogin.isLogin(context)){
            joinPoint.proceed();
        }else{
            iLogin.login(context,loginFilterAnn.userDefine());
        }
    }
}
  • 定義登錄接口
public interface ILogin {

    /**
     * 登錄事件接收
     * @param applicationContext
     * @param userDefine
     */
    void login(Context applicationContext, int userDefine);

    /**
     * 判斷是否登錄
     * @param applicationContext
     * @return
     */
    boolean isLogin(Context applicationContext);

    /**
     * 清楚登錄狀態
     * @return
     */
    void clearLoginStatus(Context applicationContext);
}

  • 定義變量簡單記錄登錄狀態
public class ContactValue {
    // 簡單記錄下登錄狀態
    public static boolean isLogin = false;
}
  • 定義登錄的輔助類
public class LoginAssistant {

    public static LoginAssistant getInstance() {
        return SingleHolder.instance;
    }

    private static final class SingleHolder {
        private final static LoginAssistant instance = new LoginAssistant();
    }


    private Context context;
    private ILogin iLogin;

    public void setContext(Context context) {
        this.context = context.getApplicationContext();
    }

    public Context getContext() {
        return context;
    }

    public ILogin getiLogin() {
        return iLogin;
    }

    public void setiLogin(ILogin iLogin) {
        this.iLogin = iLogin;
    }
}

  • 定義 SDK用於初始化
public class LoginSDK {
    private LoginSDK() {
    }

    public static LoginSDK getInstance() {
        return InstanceHolder.INSTANCE;
    }

    private final static class InstanceHolder {
        private static LoginSDK INSTANCE = new LoginSDK();
    }

    public void init(Context context,ILogin login){
        LoginAssistant.getInstance().setContext(context.getApplicationContext());
        LoginAssistant.getInstance().setiLogin(login);
    }

}

  • MainActivity調用
public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

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

    /**
     * 需要登錄的功能1 點擊事件
     */
    @LoginFilterAnn()
    public void shark(View view) {
    	// 	隨便寫個跳轉的頁面
        Intent intent = new Intent(this, SearchActivity.class);
        startActivity(intent);
    }

    /**
     * 需要登錄的功能2 點擊事件
     */
    @LoginFilterAnn(userDefine = 1)
    public void scan(View view) {
    	// 	隨便寫個跳轉的頁面
        Intent intent = new Intent(this, SettingActivity.class);
        startActivity(intent);
    }
	// 清除登錄狀態 點擊事件
    public void clear(View view) {
        ContactValue.isLogin = false;
    }
}

  • Application中初始化
public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        init();
    }

    private void init() {
        ILogin iLogin = new ILogin() {
            @Override
            public void login(Context applicationContext, int userDefine) {

                switch (userDefine) {
                    case 0:
                        Intent intent = new Intent(applicationContext, LoginActivity.class);
                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                        applicationContext.startActivity(intent);
                        break;
                    case 1:
                        Toast.makeText(applicationContext, "請登錄", Toast.LENGTH_SHORT).show();
                        break;
                    case 2:
                        Toast.makeText(applicationContext, "2", Toast.LENGTH_SHORT).show();
                        break;
                }
            }

            @Override
            public boolean isLogin(Context applicationContext) {
                return ContactValue.isLogin;
            }

            @Override
            public void clearLoginStatus(Context applicationContext) {
                ContactValue.isLogin = false;
            }
        };
        LoginSDK.getInstance().init(this, iLogin);
    }
}

效果就是直接在點擊跳轉事件上添加註解,就可以統一處理是否登錄的跳轉邏輯了。
最後那個例子下載源碼點擊這裏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章