Android 中的 AOP 編程
- 原文作者 : Fernando Cejas
- 譯文出自 : 開(kāi)發(fā)技術(shù)前線 www.devtf.cn
- 譯者 : byronwind
- 校對(duì)者: bboyfeiyu
- 狀態(tài) : 校對(duì)完成
面向切面編程(AOP,Aspect-oriented programming)需要把程序邏輯分解成『關(guān)注點(diǎn)』(concerns,功能的內(nèi)聚區(qū)域)。這意味著,在 AOP 中,我們不需要顯式的修改就可以向代碼中添加可執(zhí)行的代碼塊。這種編程范式假定『橫切關(guān)注點(diǎn)』(cross-cutting concerns,多處代碼中需要的邏輯,但沒(méi)有一個(gè)單獨(dú)的類來(lái)實(shí)現(xiàn))應(yīng)該只被實(shí)現(xiàn)一次,且能夠多次注入到需要該邏輯的地方。
代碼注入是 AOP 中的重要部分:它在處理上述提及的橫切整個(gè)應(yīng)用的『關(guān)注點(diǎn)』時(shí)很有用,例如日志或者性能監(jiān)控。這種方式,并不如你所想的應(yīng)用甚少,相反的,每個(gè)程序員都可以有使用這種注入代碼能力的場(chǎng)景,這樣可以避免很多痛苦和無(wú)奈。
AOP 是一種已經(jīng)存在了很多年的編程范式。我發(fā)現(xiàn)把它應(yīng)用到 Android 開(kāi)發(fā)中也很有用。經(jīng)過(guò)一番調(diào)研后,我認(rèn)為我們用它可以獲得很多好處和有用的東西。
術(shù)語(yǔ)(迷你術(shù)語(yǔ)表)
在開(kāi)始之前,我們先看看需要了解的詞匯:
Cross-cutting concerns(橫切關(guān)注點(diǎn)): 盡管面向?qū)ο竽P椭写蠖鄶?shù)類會(huì)實(shí)現(xiàn)單一特定的功能,但通常也會(huì)開(kāi)放一些通用的附屬功能給其他類。例如,我們希望在數(shù)據(jù)訪問(wèn)層中的類中添加日志,同時(shí)也希望當(dāng)UI層中一個(gè)線程進(jìn)入或者退出調(diào)用一個(gè)方法時(shí)添加日志。盡管每個(gè)類都有一個(gè)區(qū)別于其他類的主要功能,但在代碼里,仍然經(jīng)常需要添加一些相同的附屬功能。
Advice(通知): 注入到class文件中的代碼。典型的 Advice 類型有 before、after 和 around,分別表示在目標(biāo)方法執(zhí)行之前、執(zhí)行后和完全替代目標(biāo)方法執(zhí)行的代碼。 除了在方法中注入代碼,也可能會(huì)對(duì)代碼做其他修改,比如在一個(gè)class中增加字段或者接口。
Joint point(連接點(diǎn)): 程序中可能作為代碼注入目標(biāo)的特定的點(diǎn),例如一個(gè)方法調(diào)用或者方法入口。
Pointcut(切入點(diǎn)): 告訴代碼注入工具,在何處注入一段特定代碼的表達(dá)式。例如,在哪些 joint points 應(yīng)用一個(gè)特定的 Advice。切入點(diǎn)可以選擇唯一一個(gè),比如執(zhí)行某一個(gè)方法,也可以有多個(gè)選擇,比如,標(biāo)記了一個(gè)定義成@DebguTrace 的自定義注解的所有方法。
Aspect(切面): Pointcut 和 Advice 的組合看做切面。例如,我們?cè)趹?yīng)用中通過(guò)定義一個(gè) pointcut 和給定恰當(dāng)?shù)腶dvice,添加一個(gè)日志切面。
Weaving(織入): 注入代碼(advices)到目標(biāo)位置(joint points)的過(guò)程。
下面這張圖簡(jiǎn)要總結(jié)了一下上述這些概念。
那么...我們何時(shí)何地應(yīng)用AOP呢?
一些示例的 cross-cutting concerns 如下:
- 日志
- 持久化
- 性能監(jiān)控
- 數(shù)據(jù)校驗(yàn)
- 緩存
- 其他更多
取決于你所選的其中一種或其他方案 :)。
工具和庫(kù)
有一些工具和庫(kù)幫助我們使用 AOP:
AspectJ: 一個(gè) JavaTM 語(yǔ)言的面向切面編程的無(wú)縫擴(kuò)展(適用Android)。
Javassist for Android: 用于字節(jié)碼操作的知名 java 類庫(kù) Javassist 的 Android 平臺(tái)移植版。
DexMaker: Dalvik 虛擬機(jī)上,在編譯期或者運(yùn)行時(shí)生成代碼的 Java API。
ASMDEX: 一個(gè)類似 ASM 的字節(jié)碼操作庫(kù),運(yùn)行在Android平臺(tái),操作Dex字節(jié)碼。
為什么用 AspectJ?
我們下面的例子選用 AspectJ,有以下原因:
- 功能強(qiáng)大
- 支持編譯期和加載時(shí)代碼注入
- 易于使用
示例
比方說(shuō),我們要測(cè)量一個(gè)方法的性能(執(zhí)行這個(gè)方法需要多長(zhǎng)時(shí)間)。為此我們用一個(gè) @DebugTrace 的注解標(biāo)記我們的這個(gè)方法,并且無(wú)需在每個(gè)注解過(guò)的方法中編寫代碼,就可以通過(guò) logcat 輸出結(jié)果。我們的方法是使用 AspectJ 達(dá)到這個(gè)目的。
我們看下在底層到底發(fā)生了什么:
- 我們?cè)诰幾g過(guò)程中增加一個(gè)新的步驟處理注解。
- 注解的方法內(nèi)會(huì)生成和注入必要的樣板代碼。
在此,我必須要提到當(dāng)我研究這些時(shí),發(fā)現(xiàn)了Jake Wharton’s Hugo Library 這個(gè)項(xiàng)目,支持做同樣的事情。因此,我重構(gòu)了我的代碼,看上去和它類似。盡管,我的代碼是一個(gè)更加原始和簡(jiǎn)化的版本(順便提一下,通過(guò)看這個(gè)項(xiàng)目的代碼,我學(xué)到了很多)。
工程結(jié)構(gòu)
我們會(huì)把一個(gè)簡(jiǎn)單的示例應(yīng)用拆分成兩個(gè) modules,第一個(gè)包含我們的 Android App 代碼,第二個(gè)是一個(gè) Android Library 工程,使用 AspectJ 織入代碼(代碼注入)。
你可能會(huì)想知道為什么我們用一個(gè) Android Library 工程,而不是用一個(gè)純的 Java Library:原因是為了使 AspectJ 能在 Android 上運(yùn)行,我們必須在編譯時(shí)做一些 hook。這只能使用 andorid-library gradle 插件完成。(先不要為此擔(dān)心,后面我會(huì)給出更多細(xì)節(jié)。)
創(chuàng)建注解
首先我們創(chuàng)建我們的Java注解。這個(gè)注解周期聲明在 class 文件上(RetentionPolicy.CLASS),可以注解構(gòu)造函數(shù)和方法(ElementType.CONSTRUCTOR 和 ElementType.METHOD)。因此,我們的 DebugTrace.java 文件看上是這樣的:
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD })
public @interface DebugTrace {}
我們的性能監(jiān)控計(jì)時(shí)類
我已經(jīng)創(chuàng)建了一個(gè)簡(jiǎn)單的計(jì)時(shí)類,包含 start/stop
方法。下面是 StopWatch.java 文件:
/**
* Class representing a StopWatch for measuring time.
*/
public class StopWatch {
private long startTime;
private long endTime;
private long elapsedTime;
public StopWatch() {
//empty
}
private void reset() {
startTime = 0;
endTime = 0;
elapsedTime = 0;
}
public void start() {
reset();
startTime = System.nanoTime();
}
public void stop() {
if (startTime != 0) {
endTime = System.nanoTime();
elapsedTime = endTime - startTime;
} else {
reset();
}
}
public long getTotalTimeMillis() {
return (elapsedTime != 0) ? TimeUnit.NANOSECONDS.toMillis(endTime - startTime) : 0;
}
}
DebugLog 類
我只是包裝了一下 “android.util.Log”,因?yàn)槲沂紫认氲降氖窍?android log 中增加更多的實(shí)用功能。下面是代碼:
/**
* Wrapper around {@link android.util.Log}
*/
public class DebugLog {
private DebugLog() {}
/**
* Send a debug log message
*
* @param tag Source of a log message.
* @param message The message you would like logged.
*/
public static void log(String tag, String message) {
Log.d(tag, message);
}
}
Aspect 類
現(xiàn)在是時(shí)候創(chuàng)建我們的 Aspect 類(TraceAspect.java)了。Aspect 類負(fù)責(zé)管理注解的處理和代碼織入。
/**
* Aspect representing the cross cutting-concern: Method and Constructor Tracing.
*/
@Aspect
public class TraceAspect {
private static final String POINTCUT_METHOD =
"execution(@org.android10.gintonic.annotation.DebugTrace * *(..))";
private static final String POINTCUT_CONSTRUCTOR =
"execution(@org.android10.gintonic.annotation.DebugTrace *.new(..))";
@Pointcut(POINTCUT_METHOD)
public void methodAnnotatedWithDebugTrace() {}
@Pointcut(POINTCUT_CONSTRUCTOR)
public void constructorAnnotatedDebugTrace() {}
@Around("methodAnnotatedWithDebugTrace() || constructorAnnotatedDebugTrace()")
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = methodSignature.getDeclaringType().getSimpleName();
String methodName = methodSignature.getName();
final StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object result = joinPoint.proceed();
stopWatch.stop();
DebugLog.log(className, buildLogMessage(methodName, stopWatch.getTotalTimeMillis()));
return result;
}
/**
* Create a log message.
*
* @param methodName A string with the method name.
* @param methodDuration Duration of the method in milliseconds.
* @return A string representing message.
*/
private static String buildLogMessage(String methodName, long methodDuration) {
StringBuilder message = new StringBuilder();
message.append("Gintonic --> ");
message.append(methodName);
message.append(" --> ");
message.append("[");
message.append(methodDuration);
message.append("ms");
message.append("]");
return message.toString();
}
}
幾個(gè)在此提到的重點(diǎn):
- 我們聲明了兩個(gè)作為 pointcuts 的 public 方法,篩選出所有通過(guò)
“org.android10.gintonic.annotation.DebugTrace”
注解的方法和構(gòu)造函數(shù)。 - 我們使用
“@Around”
注解定義了“weaveJointPoint(ProceedingJoinPoint joinPoint)”
方法,使我們的代碼注入在使用"@DebugTrace"
注解的地方生效。 -
“Object result = joinPoint.proceed();”
這行代碼是被注解的方法執(zhí)行的地方。因此,在此之前,我們啟動(dòng)我們的計(jì)時(shí)類計(jì)時(shí),在這之后,停止計(jì)時(shí)。 - 最后,我們構(gòu)造日志信息,用 Android Log 輸出。
使 AspectJ 運(yùn)行在 Anroid 上
現(xiàn)在,所有代碼都可以正常工作了,但是,如果我們編譯我們的例子,我們并沒(méi)有看到任何事情發(fā)生。原因是我們必須使用 AspectJ 的編譯器(ajc,一個(gè)java編譯器的擴(kuò)展)對(duì)所有受 aspect 影響的類進(jìn)行織入。這就是為什么,我之前提到的,我們需要在 gradle 的編譯 task 中增加一些額外配置,使之能正確編譯運(yùn)行。
我們的 build.gradle 文件如下:
import com.android.build.gradle.LibraryPlugin
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:0.12.+'
classpath 'org.aspectj:aspectjtools:1.8.1'
}
}
apply plugin: 'android-library'
repositories {
mavenCentral()
}
dependencies {
compile 'org.aspectj:aspectjrt:1.8.1'
}
android {
compileSdkVersion 19
buildToolsVersion '19.1.0'
lintOptions {
abortOnError false
}
}
android.libraryVariants.all { variant ->
LibraryPlugin plugin = project.plugins.getPlugin(LibraryPlugin)
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", plugin.project.android.bootClasspath.join(
File.pathSeparator)]
MessageHandler handler = new MessageHandler(true);
new Main().run(args, handler)
def log = project.logger
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:
case IMessage.INFO:
log.info message.message, message.thrown
break;
case IMessage.DEBUG:
log.debug message.message, message.thrown
break;
}
}
}
}
我們的測(cè)試方法
我們添加一個(gè)測(cè)試方法,來(lái)使用我們炫酷的 aspect 注解。我已經(jīng)在主 Activity 類中增加了一個(gè)方法用來(lái)測(cè)試。看下代碼:
@DebugTrace
private void testAnnotatedMethod() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
運(yùn)行我們的應(yīng)用
我們用 gradle 命令編譯部署我們的 app 到 android 設(shè)備或者模擬器上:
gradlew clean build installDebug
If we open the logcat and execute our sample, we will see a debug log with:
如果我們打開(kāi) logcat,執(zhí)行我們的例子,會(huì)看到一條 debug 日志:
Gintonic --> testAnnotatedMethod --> [10ms]
我們的第一個(gè)使用 AOP 的 Androd 應(yīng)用可以工作了!
你可以用 Dex Dump 或者任何其他的逆向工具反編譯 apk 文件,看一下生成和注入的代碼。
回顧
回顧總結(jié)如下:
- 我們已經(jīng)對(duì)面向切面編程(AOP)這一范式有了初步體驗(yàn)。
- 代碼注入是 AOP 中的重要部分。
- AspectJ 是在 Android 應(yīng)用中進(jìn)行代碼織入的強(qiáng)大且易用的工具。
- 我們已經(jīng)使用 AOP 能力創(chuàng)建了一個(gè)可以工作的示例。
結(jié)論
面向切面編程很強(qiáng)大。通過(guò)正確使用,你可以在開(kāi)發(fā)你的 Android 應(yīng)用時(shí),避免在『cross-cutting concerns』處復(fù)制大量代碼,比如我們?cè)谑纠锌吹降男阅鼙O(jiān)控部分。我非常鼓勵(lì)你嘗試一下,你會(huì)發(fā)現(xiàn)它非常有用。
我希望你能喜歡這篇文章,文章的目的是分享我學(xué)到的東西,所以,歡迎評(píng)論和反饋,如果能 fork 代碼玩一下就更好了。
我確信我們能在示例 app 的 AOP 模塊里增加些有趣的東西,歡迎提出你的想法;)。
源碼
你可以在 https://github.com/android10/Android-AOPExample 下載示例 app 代碼。另外我還有一個(gè)使用動(dòng)態(tài)代理的 Java AOP 示例(也可以用在Android上):https://github.com/android10/DynamicProxy_Java_Sample。
資源
- Aspect-oriented programming.
- Aspect-oriented software development.
- Practical Introduction into Code Injection with AspectJ, Javassist, and Java Proxy.
- Implementing Build-time Bytecode Instrumentation With Javassist.
- Frequently Asked Questions about AspectJ.
- AspectJ Cheat Sheet.
譯注
- AOP 中的術(shù)語(yǔ)并沒(méi)有統(tǒng)一的中文翻譯,翻譯過(guò)程中,術(shù)語(yǔ)一節(jié)我選取了用的比較多的中文名稱注釋在括號(hào)中幫助理解,正文中其他部分出現(xiàn)的術(shù)語(yǔ),使用原始英文命名。
- 這篇文章是2014年發(fā)布的,2015年7月,阿里巴巴剛剛開(kāi)源了一個(gè)強(qiáng)大的 Android 平臺(tái) AOP 框架 Dexposed,該項(xiàng)目基于著名的 Xposed 項(xiàng)目。