- 原文链接 : Aspect Oriented Programming in Android
- 原文作者 : Fernando Cejas
- 译文出自 : 开发技术前线 www.devtf.cn
- 译者 : byronwind
- 校对者: bboyfeiyu
- 状态 : 校对完成
面向切面编程(AOP,Aspect-oriented programming)需要把程序逻辑分解成『关注点』(concerns,功能的内聚区域)。这意味着,在 AOP 中,我们不需要显式的修改就可以向代码中添加可执行的代码块。这种编程范式假定『横切关注点』(cross-cutting concerns,多处代码中需要的逻辑,但没有一个单独的类来实现)应该只被实现一次,且能够多次注入到需要该逻辑的地方。
代码注入是 AOP 中的重要部分:它在处理上述提及的横切整个应用的『关注点』时很有用,例如日志或者性能监控。这种方式,并不如你所想的应用甚少,相反的,每个程序员都可以有使用这种注入代码能力的场景,这样可以避免很多痛苦和无奈。
AOP 是一种已经存在了很多年的编程范式。我发现把它应用到 Android 开发中也很有用。经过一番调研后,我认为我们用它可以获得很多好处和有用的东西。
在开始之前,我们先看看需要了解的词汇:
Cross-cutting concerns(横切关注点): 尽管面向对象模型中大多数类会实现单一特定的功能,但通常也会开放一些通用的附属功能给其他类。例如,我们希望在数据访问层中的类中添加日志,同时也希望当UI层中一个线程进入或者退出调用一个方法时添加日志。尽管每个类都有一个区别于其他类的主要功能,但在代码里,仍然经常需要添加一些相同的附属功能。
Advice(通知): 注入到class文件中的代码。典型的 Advice 类型有 before、after 和 around,分别表示在目标方法执行之前、执行后和完全替代目标方法执行的代码。 除了在方法中注入代码,也可能会对代码做其他修改,比如在一个class中增加字段或者接口。
Joint point(连接点): 程序中可能作为代码注入目标的特定的点,例如一个方法调用或者方法入口。
Pointcut(切入点): 告诉代码注入工具,在何处注入一段特定代码的表达式。例如,在哪些 joint points 应用一个特定的 Advice。切入点可以选择唯一一个,比如执行某一个方法,也可以有多个选择,比如,标记了一个定义成@DebguTrace 的自定义注解的所有方法。
Aspect(切面): Pointcut 和 Advice 的组合看做切面。例如,我们在应用中通过定义一个 pointcut 和给定恰当的advice,添加一个日志切面。
Weaving(织入): 注入代码(advices)到目标位置(joint points)的过程。
下面这张图简要总结了一下上述这些概念。
一些示例的 cross-cutting concerns 如下:
取决于你所选的其中一种或其他方案 :)。
有一些工具和库帮助我们使用 AOP:
AspectJ: 一个 JavaTM 语言的面向切面编程的无缝扩展(适用Android)。
Javassist for Android: 用于字节码操作的知名 java 类库 Javassist 的 Android 平台移植版。
DexMaker: Dalvik 虚拟机上,在编译期或者运行时生成代码的 Java API。
ASMDEX: 一个类似 ASM 的字节码操作库,运行在Android平台,操作Dex字节码。
我们下面的例子选用 AspectJ,有以下原因:
比方说,我们要测量一个方法的性能(执行这个方法需要多长时间)。为此我们用一个@DebugTrace 的注解标记我们的这个方法,并且无需在每个注解过的方法中编写代码,就可以通过 logcat 输出结果。我们的方法是使用 AspectJ 达到这个目的。
我们看下在底层到底发生了什么:
在此,我必须要提到当我研究这些时,发现了Jake Wharton’s Hugo Library 这个项目,支持做同样的事情。因此,我重构了我的代码,看上去和它类似。尽管,我的代码是一个更加原始和简化的版本(顺便提一下,通过看这个项目的代码,我学到了很多)。
我们会把一个简单的示例应用拆分成两个 modules,第一个包含我们的 Android App 代码,第二个是一个 Android Library 工程,使用 AspectJ 织入代码(代码注入)。
你可能会想知道为什么我们用一个 Android Library 工程,而不是用一个纯的 Java Library:原因是为了使 AspectJ 能在 Android 上运行,我们必须在编译时做一些 hook。这只能使用 andorid-library gradle 插件完成。(先不要为此担心,后面我会给出更多细节。)
首先我们创建我们的Java注解。这个注解周期声明在 class 文件上(RetentionPolicy.CLASS),可以注解构造函数和方法(ElementType.CONSTRUCTOR 和 ElementType.METHOD)。因此,我们的 DebugTrace.java 文件看上是这样的:
1
2
3
4
|
@
Retention
(
RetentionPolicy
.
CLASS
)
@
Target
(
{
ElementType
.
CONSTRUCTOR
,
ElementType
.
METHOD
}
)
public
@
interface
DebugTrace
{
}
|
我已经创建了一个简单的计时类,包含 start/stop
方法。下面是 StopWatch.java 文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
/**
* 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
;
}
}
|
我只是包装了一下 “android.util.Log”,因为我首先想到的是向 android log 中增加更多的实用功能。下面是代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
/**
* 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 类(TraceAspect.java)了。Aspect 类负责管理注解的处理和代码织入。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
/**
* 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
(
)
;
}
}
|
几个在此提到的重点:
“org.android10.gintonic.annotation.DebugTrace”
注解的方法和构造函数。“@Around”
注解定义了“weaveJointPoint(ProceedingJoinPoint joinPoint)”
方法,使我们的代码注入在使用"@DebugTrace"
注解的地方生效。“Object result = joinPoint.proceed();”
这行代码是被注解的方法执行的地方。因此,在此之前,我们启动我们的计时类计时,在这之后,停止计时。现在,所有代码都可以正常工作了,但是,如果我们编译我们的例子,我们并没有看到任何事情发生。原因是我们必须使用 AspectJ 的编译器(ajc,一个java编译器的扩展)对所有受 aspect 影响的类进行织入。这就是为什么,我之前提到的,我们需要在 gradle 的编译 task 中增加一些额外配置,使之能正确编译运行。
我们的 build.gradle 文件如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
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
;
}
}
}
}
|
我们添加一个测试方法,来使用我们炫酷的 aspect 注解。我已经在主 Activity 类中增加了一个方法用来测试。看下代码:
1
2
3
4
5
6
7
8
9
|
@
DebugTrace
private
void
testAnnotatedMethod
(
)
{
try
{
Thread
.
sleep
(
10
)
;
}
catch
(
InterruptedException
e
)
{
e
.
printStackTrace
(
)
;
}
}
|
我们用 gradle 命令编译部署我们的 app 到 android 设备或者模拟器上:
1
2
|
gradlew
clean
build
installDebug
|
If we open the logcat and execute our sample, we will see a debug log with:
如果我们打开 logcat,执行我们的例子,会看到一条 debug 日志:
1
2
|
Gintonic
--
>
testAnnotatedMethod
--
>
[
10ms
]
|
我们的第一个使用 AOP 的 Androd 应用可以工作了!
你可以用 Dex Dump 或者任何其他的逆向工具反编译 apk 文件,看一下生成和注入的代码。
回顾总结如下:
面向切面编程很强大。通过正确使用,你可以在开发你的 Android 应用时,避免在『cross-cutting concerns』处复制大量代码,比如我们在示例中看到的性能监控部分。我非常鼓励你尝试一下,你会发现它非常有用。
我希望你能喜欢这篇文章,文章的目的是分享我学到的东西,所以,欢迎评论和反馈,如果能 fork 代码玩一下就更好了。
我确信我们能在示例 app 的 AOP 模块里增加些有趣的东西,欢迎提出你的想法;)。
你可以在 https://github.com/android10/Android-AOPExample 下载示例 app 代码。另外我还有一个使用动态代理的 Java AOP 示例(也可以用在Android上):https://github.com/android10/Android-AOPExample
译注
- AOP 中的术语并没有统一的中文翻译,翻译过程中,术语一节我选取了用的比较多的中文名称注释在括号中帮助理解,正文中其他部分出现的术语,使用原始英文命名。
- 这篇文章是2014年发布的,2015年7月,阿里巴巴刚刚开源了一个强大的 Android 平台 AOP 框架 Dexposed,该项目基于著名的 Xposed 项目。