AOP 是什么
在軟件業,AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,是函數式編程的一種衍生范型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。
它是一種關注點分離的技術。我們軟件開發時經常提一個詞叫做“業務邏輯”或者“業務功能”,我們的代碼主要就是實現某種特定的業務邏輯。但是我們往往不能專注于業務邏輯,比如我們寫業務邏輯代碼的同時,還要寫事務管理、緩存、日志等等通用化的功能,而且每個業務功能都要和這些業務功能混在一起,非常非常地痛苦。為了將業務功能的關注點和通用化功能的關注點分離開來,就出現了AOP技術。
AOP 和 OOP
面向對象的特點是繼承、多態和封裝。為了符合單一職責的原則,OOP將功能分散到不同的對象中去。讓不同的類設計不同的方法,這樣代碼就分散到一個個的類中。可以降低代碼的復雜程度,提高類的復用性。
但是在分散代碼的同時,也增加了代碼的重復性。比如說,我們在兩個類中,可能都需要在每個方法中做日志。按照OOP的設計方法,我們就必須在兩個類的方法中都加入日志的內容。也許他們是完全相同的,但是因為OOP的設計讓類與類之間無法聯系,而不能將這些重復的代碼統一起來。然而AOP就是為了解決這類問題而產生的,它是在運行時動態地將代碼切入到類的指定方法、指定位置上的編程思想。
如果說,面向過程的編程是一維的,那么面向對象的編程就是二維的。OOP從橫向上區分出一個個的類,相比過程式增加了一個維度。而面向切面結合面向對象編程是三維的,相比單單的面向對象編程則又增加了“方面”的維度。從技術上來說,AOP基本上是通過代理機制實現的。
AOP 在 Android 開發中的常見用法
我封裝的 library 已經把常用的 Android AOP 用法概況在其中
github地址:https://github.com/fengzhizi715/SAF-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 的原理如下, 借助 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 只適用于 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;
}
}
目前,方法的入參和出參只支持基本類型和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的原理如下, 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 只是把錯誤的日志信息打印出來。
我們來看看,@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注解,就可以實現追蹤某個方法的耗時。如果耗時過長那就需要優化代碼,優化完了再進行測試。
當然啦,在生產環境中不建議使用這樣的注解。
總結
AOP 是 OOP 的有力補充。玩好 AOP 對開發 App 是有很大的幫助的,當然也可以直接使用我的庫:),而且新的使用方法我也會不斷地更新。由于水平有限,如果有任何地方闡述地不正確,歡迎指出,我好及時修改:)