1. 程式人生 > >歸納AOP在Android開發中的幾種常見用法

歸納AOP在Android開發中的幾種常見用法

AOP 是什麼

在軟體業,AOP為Aspect Oriented Programming的縮寫,意為:面向切面程式設計,通過預編譯方式和執行期動態代理實現程式功能的統一維護的一種技術。AOP是OOP的延續,是軟體開發中的一個熱點,是函數語言程式設計的一種衍生範型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。

它是一種關注點分離的技術。我們軟體開發時經常提一個詞叫做“業務邏輯”或者“業務功能”,我們的程式碼主要就是實現某種特定的業務邏輯。但是我們往往不能專注於業務邏輯,比如我們寫業務邏輯程式碼的同時,還要寫事務管理、快取、日誌等等通用化的功能,而且每個業務功能都要和這些業務功能混在一起,非常非常地痛苦。為了將業務功能的關注點和通用化功能的關注點分離開來,就出現了AOP技術。

AOP 和 OOP

面向物件的特點是繼承、多型和封裝。為了符合單一職責的原則,OOP將功能分散到不同的物件中去。讓不同的類設計不同的方法,這樣程式碼就分散到一個個的類中。可以降低程式碼的複雜程度,提高類的複用性。 

但是在分散程式碼的同時,也增加了程式碼的重複性。比如說,我們在兩個類中,可能都需要在每個方法中做日誌。按照OOP的設計方法,我們就必須在兩個類的方法中都加入日誌的內容。也許他們是完全相同的,但是因為OOP的設計讓類與類之間無法聯絡,而不能將這些重複的程式碼統一起來。然而AOP就是為了解決這類問題而產生的,它是在執行時動態地將程式碼切入到類的指定方法、指定位置上的程式設計思想。

如果說,面向過程的程式設計是一維的,那麼面向物件的程式設計就是二維的。OOP從橫向上區分出一個個的類,相比過程式增加了一個維度。而面向切面結合面向物件程式設計是三維的,相比單單的面向物件程式設計則又增加了“方面”的維度。從技術上來說,AOP基本上是通過代理機制實現的。


AOPConcept.JPG

AOP 在 Android 開發中的常見用法

我封裝的 library 已經把常用的 Android AOP 用法概況在其中

0. 下載和安裝

在根目錄下的build.gradle中新增

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

在app 模組目錄下的build.gradle中新增

apply plugin: 'com.hujiang.android-aspectjx'

...

dependencies {
    compile 'com.safframework:saf-aop:1.0.0'
    ...
}

1. 非同步執行app中的方法

告別Thread、Handler、BroadCoast等方式更簡單的執行非同步方法。只需在目標方法上標註@Async

import android.app.Activity;
import android.os.Bundle;
import android.os.Looper;
import android.widget.Toast;

import com.safframework.app.annotation.Async;
import com.safframework.log.L;

/**
 * Created by Tony Shen on 2017/2/7.
 */

public class DemoForAsyncActivity extends Activity {

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

        initData();
    }

    @Async
    private void initData() {

        StringBuilder sb = new StringBuilder();
        sb.append("current thread=").append(Thread.currentThread().getId())
                .append("\r\n")
                .append("ui thread=")
                .append(Looper.getMainLooper().getThread().getId());


        Toast.makeText(DemoForAsyncActivity.this, sb.toString(), Toast.LENGTH_SHORT).show();
        L.i(sb.toString());
    }
}

可以清晰地看到當前的執行緒和UI執行緒是不一樣的。


@Async執行結果.png

@Async 的原理如下, 藉助 Rxjava 實現非同步方法。

import android.os.Looper;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

import rx.Observable;
import rx.Subscriber;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;

/**
 * Created by Tony Shen on 16/3/23.
 */
@Aspect
public class AsyncAspect {

    @Around("execution(!synthetic * *(..)) && onAsyncMethod()")
    public void doAsyncMethod(final ProceedingJoinPoint joinPoint) throws Throwable {
        asyncMethod(joinPoint);
    }

    @Pointcut("@within(com.safframework.app.annotation.Async)||@annotation(com.safframework.app.annotation.Async)")
    public void onAsyncMethod() {
    }

    private void asyncMethod(final ProceedingJoinPoint joinPoint) throws Throwable {

        Observable.create(new Observable.OnSubscribe<Object>() {

            @Override
            public void call(Subscriber<? super Object> subscriber) {
                Looper.prepare();
                try {
                    joinPoint.proceed();
                } catch (Throwable throwable) {
                    throwable.printStackTrace();
                }
                Looper.loop();
            }
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe();
    }
}

2. 將方法返回的結果放於快取中

我先給公司的後端專案寫了一個 CouchBase 的註解,該註解是藉助 Spring Cache和 CouchBase 結合的自定義註解,可以把某個方法返回的結果直接放入 CouchBase 中,簡化了 CouchBase 的操作。讓開發人員更專注於業務程式碼。

受此啟發,我寫了一個 Android 版本的註解,來看看該註解是如何使用的。

import android.app.Activity;
import android.os.Bundle;
import android.widget.Toast;

import com.safframework.app.annotation.Cacheable;
import com.safframework.app.domain.Address;
import com.safframework.cache.Cache;
import com.safframework.injectview.Injector;
import com.safframework.injectview.annotations.OnClick;
import com.safframework.log.L;
import com.safframwork.tony.common.utils.StringUtils;

/**
 * Created by Tony Shen on 2017/2/7.
 */

public class DemoForCacheableActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo_for_cacheable);
        Injector.injectInto(this);

        initData();
    }

    @Cacheable(key = "address")
    private Address initData() {

        Address address = new Address();
        address.country = "China";
        address.province = "Jiangsu";
        address.city = "Suzhou";
        address.street = "Ren min Road";

        return address;
    }

    @OnClick(id={R.id.text})
    void clickText() {

        Cache cache = Cache.get(this);
        Address address = (Address) cache.getObject("address");
        Toast.makeText(this, StringUtils.printObject(address),Toast.LENGTH_SHORT).show();
        L.json(address);
    }
}

在 initData() 上標註 @Cacheable 註解和快取的key,點選text按鈕之後,就會打印出快取的資料和 initData() 存入的資料是一樣的。


@Cacheable執行結果.png

目前,該註解 @Cacheable 只適用於 Android 4.0以上。

3. 將方法返回的結果放入SharedPreferences中

該註解 @Prefs 的用法跟上面 @Cacheable 類似,區別是將結果放到SharedPreferences。

同樣,該註解 @Prefs 也只適用於 Android 4.0以上

4. App 除錯時,將方法的入參和出參都打印出來

在除錯時,如果一眼無法看出錯誤在哪裡,那肯定會把一些關鍵資訊打印出來。
在 App 的任何方法上標註 @LogMethod,可以實現剛才的目的。

public class DemoForLogMethodActivity extends Activity{

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

        initData1();

        initData2("test");

        User u = new User();
        u.name = "tony";
        u.password = "123456";
        initData3(u);
    }

    @LogMethod
    private void initData1() {
    }

    @LogMethod
    private String initData2(String s) {

        return s;
    }

    @LogMethod
    private User initData3(User u) {

        u.password = "abcdefg";

        return u;
    }
}

@LogMethod執行結果.png

目前,方法的入參和出參只支援基本型別和String,未來我會加上支援任意物件的列印以及優雅地展現出來。

5. 在呼叫某個方法之前、以及之後進行hook

通常,在 App 的開發過程中會在一些關鍵的點選事件、按鈕、頁面上進行埋點,方便資料分析師、產品經理在後臺能夠檢視和分析。

以前在大的電商公司,每次 App 發版之前,都要跟資料分析師一起過一下看看哪些地方需要進行埋點。發版在即,新增程式碼會非常倉促,還需要安排人手進行測試。而且埋點的程式碼都很通用,所以產生了 @Hook 這個註解。它可以在呼叫某個方法之前、以及之後進行hook。可以單獨使用也可以跟任何自定義註解配合使用。

    @HookMethod(beforeMethod = "method1",afterMethod = "method2")
    private void initData() {

        L.i("initData()");
    }

    private void method1() {
        L.i("method1() is called before initData()");
    }

    private void method2() {
        L.i("method2() is called after initData()");
    }

來看看列印的結果,不出意外先列印method1() is called before initData(),再列印initData(),最後列印method2() is called after initData()。


@Hook執行的結果.png

@Hook的原理如下, beforeMethod和afterMethod即使找不到或者沒有定義也不會影響原先方法的使用。

import com.safframework.app.annotation.HookMethod;
import com.safframework.log.L;
import com.safframwork.tony.common.reflect.Reflect;
import com.safframwork.tony.common.reflect.ReflectException;
import com.safframwork.tony.common.utils.Preconditions;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.reflect.Method;


/**
 * Created by Tony Shen on 2016/12/7.
 */
@Aspect
public class HookMethodAspect {

    @Around("execution(!synthetic * *(..)) && onHookMethod()")
    public void doHookMethodd(final ProceedingJoinPoint joinPoint) throws Throwable {
        hookMethod(joinPoint);
    }

    @Pointcut("@within(com.safframework.app.annotation.HookMethod)||@annotation(com.safframework.app.annotation.HookMethod)")
    public void onHookMethod() {
    }

    private void hookMethod(final ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        HookMethod hookMethod = method.getAnnotation(HookMethod.class);

        if (hookMethod==null) return;

        String beforeMethod = hookMethod.beforeMethod();
        String afterMethod = hookMethod.afterMethod();

        if (Preconditions.isNotBlank(beforeMethod)) {
            try {
                Reflect.on(joinPoint.getTarget()).call(beforeMethod);
            } catch (ReflectException e) {
                e.printStackTrace();
                L.e("no method "+beforeMethod);
            }
        }

        joinPoint.proceed();

        if (Preconditions.isNotBlank(afterMethod)) {
            try {
                Reflect.on(joinPoint.getTarget()).call(afterMethod);
            } catch (ReflectException e) {
                e.printStackTrace();
                L.e("no method "+afterMethod);
            }
        }
    }
}

6. 安全地執行方法,不用考慮異常情況

一般情況,寫下這樣的程式碼肯定會丟擲空指標異常,從而導致App Crash。

    private void initData() {

        String s = null;
        int length = s.length();
    }

然而,使用 @Safe 可以確保即使遇到異常,也不會導致 App Crash,給 App 帶來更好的使用者體驗。

    @Safe
    private void initData() {

        String s = null;
        int length = s.length();
    }

再看一下logcat的日誌,App 並沒有 Crash 只是把錯誤的日誌資訊打印出來。


logcat的日誌.png

我們來看看,@Safe的原理,在遇到異常情況時直接catch Throwable。

import com.safframework.log.L;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

import java.io.PrintWriter;
import java.io.StringWriter;

/**
 * Created by Tony Shen on 16/3/23.
 */
@Aspect
public class SafeAspect {

    @Around("execution(!synthetic * *(..)) && onSafe()")
    public Object doSafeMethod(final ProceedingJoinPoint joinPoint) throws Throwable {
        return safeMethod(joinPoint);
    }

    @Pointcut("@within(com.safframework.app.annotation.Safe)||@annotation(com.safframework.app.annotation.Safe)")
    public void onSafe() {
    }

    private Object safeMethod(final ProceedingJoinPoint joinPoint) throws Throwable {

        Object result = null;
        try {
            result = joinPoint.proceed(joinPoint.getArgs());
        } catch (Throwable e) {
            L.w(getStringFromException(e));
        }
        return result;
    }

    private static String getStringFromException(Throwable ex) {
        StringWriter errors = new StringWriter();
        ex.printStackTrace(new PrintWriter(errors));
        return errors.toString();
    }
}

7. 追蹤某個方法花費的時間,用於效能調優

無論是開發 App 還是 Service 端,我們經常會用做一些效能方面的測試,比如檢視某些方法的耗時。從而方便開發者能夠做一些優化的工作。@Trace 就是為這個目的而產生的。

    @Trace
    private void initData() {

        for (int i=0;i<10000;i++) {
            Map map = new HashMap();
            map.put("name","tony");
            map.put("age","18");
            map.put("gender","male");
        }
    }

來看看,這段程式碼的執行結果,日誌記錄花費了3ms。


@Trace執行結果.png

只需一個@Trace註解,就可以實現追蹤某個方法的耗時。如果耗時過長那就需要優化程式碼,優化完了再進行測試。
當然啦,在生產環境中不建議使用這樣的註解。