文章對應(yīng)的項目地址aop-tech捆毫,運行一下sample闪湾,結(jié)合代碼和文章,你會收獲更多绩卤。
熟悉程序開發(fā)的都知道OOP(Object Oriented Programming 响谓,面向?qū)ο缶幊蹋压δ芊庋b在一個類中省艳,使用的時候創(chuàng)建該類的對象娘纷,調(diào)用對象的方法或者使用其屬性即可,OOP具有可重用性跋炕、靈活性和擴展性赖晶。
盡管OOP具有很多好處,但是如果在軟件開發(fā)領(lǐng)域只使用OOP辐烂,在某些情況下也會使程序變得復(fù)雜且難以維護遏插。例如,我們需要統(tǒng)計程序中點擊事件的執(zhí)行情況纠修,如果我們要自己找遍代碼中的點擊事件胳嘲,這個工程量就太大了,而且維護起來也不方便扣草。這個時候了牛,使用AOP的方式就會使問題變得簡單。
AOP(Aspect Oriented Programming辰妙,面向切面編程)鹰祸,把某一類問題集中在一個地方進行處理,比如處理程序中的點擊事件密浑、打印日志等蛙婴。
關(guān)于OOP和AOP,我覺得鄧凡平老師在深入理解Android之AOP中說的挺對的:
OOP和AOP都是方法論尔破,表示的是我們從什么角度來看待問題街图。OOP的精髓是把功能或問題模塊化,每個模塊處理自己的家務(wù)事懒构。但在現(xiàn)實世界中餐济,并不是所有功能都能完美得劃分到模塊中。AOP的目標(biāo)是把這些功能集中起來痴脾,放到一個統(tǒng)一的地方來控制和管理颤介。
那么在Android中有哪些使用到了AOP這種思想呢梳星?
在Application中有個ActivityLifecycleCallbacks接口赞赖,這個接口提供了Activity生命周期相關(guān)的方法回調(diào)滚朵。當(dāng)開發(fā)者調(diào)用了Application的public void registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks callback)
方法之后,就可以在ActivityLifecycleCallbacks的實現(xiàn)類中統(tǒng)一處理這些生命周期方法前域。這其實就是AOP思想的一種體現(xiàn)辕近。
另外,我們今天的主角——AspectJ匿垄, 它是AOP編程思想的一個很火的實踐移宅。
AspectJ 介紹
AspectJ是一個面向切面編程的框架,它擴展了Java語言椿疗。AspectJ定義了AOP語法漏峰,它有一個專門的編譯器用來生成遵守Java字節(jié)編碼規(guī)范的Class文件。AspectJ還支持原生的Java届榄,只需要加上AspectJ提供的注解即可浅乔。在Android開發(fā)中,一般就用它提供的注解和一些簡單的語法就可以實現(xiàn)絕大部分功能上的需求了铝条。
Join Points介紹 **
Join Points靖苇,簡稱JPoints,是AspectJ中最關(guān)鍵的一個概念班缰,表示的是程序運行時的一些執(zhí)行點**贤壁。理論上說,一個程序中很多地方都可以被看做是JPoint埠忘,但是AspectJ中脾拆,只有幾種執(zhí)行點被認為是JPoints,如構(gòu)造方法調(diào)用莹妒、方法調(diào)用假丧、方法執(zhí)行、異常等等动羽。JPoints實際上就是表示想把AspectJ的代碼插入到程序哪個地方包帚,是插入在方法中,還是插入在方法調(diào)用前后运吓。需要說明的是:在AspectJ中渴邦,方法調(diào)用(call)和方法執(zhí)行(execution)是不一樣的,這個后面再做介紹拘哨。
Pointcuts介紹
一個程序會有很多的JPoints谋梭,即使是同一個函數(shù),還分為call類型和execution類型的JPoint倦青,但是并不是所有的JPoint都是我們需要關(guān)心的瓮床。比如我們可能只需要關(guān)心點擊事件方法,那么如何從眾多的JPoints中選擇我們感興趣的JPoint呢?這個時候可以用Pointcut:
@Around("execution(* android.view.View.OnClickListener.onClick(..))")
public void onClickMethodAround(ProceedingJoinPoint joinPoint) {}
上述代碼的意思就是在OnClickListener.onClick()方法執(zhí)行前后執(zhí)行代碼塊中的邏輯隘庄。
所以在這里踢步,我們可以簡單的理解Pointcut的作用就是過濾JPoint。
Advice介紹
Advice簡單來說就是表示AspectJ的hook點丑掺,在AspectJ中常用的是before获印、after、around等街州。before表示在JPoint執(zhí)行之前兼丰,需要干的事情。after表示的是在JPoint執(zhí)行之后唆缴,around表示的是在JPoint執(zhí)行前后鳍征。
Aspect介紹
前面我們講了AspectJ中使用過程中需要用到了一個概念,對于問題的處理需要統(tǒng)一放到一個地方去處理面徽,這個地方就是Aspect蟆技,意為“切面”。在Java開發(fā)中主要是使用@Aspect注解來表示一個切面斗忌。
Android 中使用Gradle集成 AspectJ
在Android中集成AspectJ质礼,主要思想就是hook Apk打包過程,使用AspectJ提供的工具來編譯.class文件织阳。這一點江锨,JakeWharton 在其項目JakeWharton/hugo 中演示了如何在Gradle中添加AspectJ攘蔽,這為后來的人指了一條光明的道路枕磁。
一般來說篡腌,自己手動接入AspectJ的話,按照下面的指示即可弄痹。
在項目根目錄build.gradle下引入aspectjtools插件:
buildscript {
dependencies {
..
classpath 'org.aspectj:aspectjtools:1.8.10'
classpath 'org.aspectj:aspectjweaver:1.8.8'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
在運行app的module目錄下的build.gradle中引入:
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
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.8",
"-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;
}
}
}
}
AspectJ在運行時也需要相關(guān)的Library支持饭入,所以還需要在項目的dependencies中添加依賴:
dependencies {
...
compile 'org.aspectj:aspectjrt:1.8.10'
}
目前還有一些在Android中集成AspectJ的比較火的框架,如 HujiangTechnology / gradle_plugin_android_aspectjx肛真。該框架支持kotlin谐丢,我對這個框架深入研究了一番,也按照它的思想寫了一個簡單的gradle plugin 蚓让,收獲頗多乾忱,我自己的項目地址是 aop-tech,項目中演示了如何通過AOP的方式解決統(tǒng)一處理登錄历极、綁定手機號窄瘟、統(tǒng)計方法耗時、打印點擊事件日志等的邏輯趟卸,有興趣的可以去看看蹄葱,歡迎交流氏义。
AspectJ 命令常用參數(shù)介紹
1 -inpath: .class文件路徑,可以是在jar文件中也可以是在文件目錄中图云,路徑應(yīng)該包含那些AspectJ相關(guān)的文件惯悠,只有這些文件才會被AspectJ處理。輸出文件會包含這些.class 琼稻。該路徑就是一個單一參數(shù),多個路徑的話用分隔符隔開饶囚。
2 -classpath: 指定去哪找用戶使用到的.class文件帕翻,路徑可以是zip文件也可以是文件目錄,該路徑就是一個單一參數(shù)萝风,多個路徑的話用分隔符隔開嘀掸。
3 -aspectPath: 需要被處理的切面路徑,存在于jar文件或者文件目錄中规惰。在Andorid中使用的話一般指的是被@Aspect注解標(biāo)示的class文件路徑睬塌。需要注意的是編譯版本需要與Java編譯版本一致。classpath指定的路徑應(yīng)該包含所有的aspectpath指定的.class文件歇万。不過默認情況下揩晴,inPath和aspectPath中的路徑不一定非要放置在classPath中,因為編譯器會自動處理把它們加入贪磺。路徑格式與classpath和inpath樣硫兰,都需要用分隔符隔開。
4 **-bootClasspath: ** 重載跟VM相關(guān)的bootClasspath寒锚,例如在Android中使用android-27的源碼進行編譯劫映。路徑格式與之前一樣。
5 -d: 指定由AspectJ處理后的.class文件存放目錄刹前,如果不指定的話會放置在當(dāng)前的工作目錄中泳赋。
6 -outjar: 指定被AspectJ處理后的jar包存放的文件目錄,
更多詳情請查看官網(wǎng) http://www.eclipse.org/aspectj/doc/released/devguide/ajc-ref.html
Sample—處理點擊事件
例如喇喉,我們需要處理項目中的所有控件的點擊事件祖今,打印控件的名稱,可以使用AspectJ來簡單方便的處理拣技。在之前已經(jīng)在gradle中引入的AspectJ的基礎(chǔ)上衅鹿,我們新建一個Java文件,如下:
@Aspect
public class ClickAspect {
private static final String TAG = "ClickAspect";
// 第一個*所在的位置表示的是返回值过咬,*表示的是任意的返回值大渤,
// onClick()中的 .. 所在位置是方法參數(shù)的位置,.. 表示的是任意類型掸绞、任意個數(shù)的參數(shù)
// * 表示的是通配
@Pointcut("execution(* android.view.View.OnClickListener.onClick(..))")
public void clickMethod() {}
@Around("clickMethod()")
public void onClickMethodAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
View view = null;
for (Object arg : args) {
if (arg instanceof View) {
view = (View) arg;
}
}
//獲取View 的 string id
String resEntryName = null;
String resName = null;
if (view != null) {
// resEntryName: btn_activity_2 resName: com.sososeen09.aop_tech:id/btn_activity_2
resEntryName = view.getContext().getResources().getResourceEntryName(view.getId());
resName = view.getContext().getResources().getResourceName(view.getId());
}
joinPoint.proceed();
Log.d(TAG, "after onclick: " + "resEntryName: " + resEntryName + " resName: " + resName);
}
}
運行項目泵三,點擊一個控件(設(shè)置了點擊事件)之后耕捞,可以看到日志輸出:
./com.sososeen09.aop_tech D/ClickAspect: after onclick: resEntryName: btn_activity_3 resName: com.sososeen09.aop_tech:id/btn_activity_3
切入點的語法
以上面的例子來講解:
- @Around:是advice,也就是具體的插入點烫幕。@Around該方法的邏輯會包含切入點前后俺抽,如果用到該注解,記得自己需要控制切入點的執(zhí)行邏輯较曼,調(diào)用
joinPoint.proceed()
磷斧。如果使用@Before注解,表示的是在切入點之前執(zhí)行捷犹,@After表示在切入點之后執(zhí)行弛饭,此時不需要調(diào)用joinPoint.proceed()
。 - execution:處理JPoint的類型萍歉,例如call侣颂、execution。對于
execution(* android.view.View.OnClickListener.onClick(..))
枪孩,第一個*
所處的位置表示的是返回值憔晒,*
是通配符,表示的是任意類型蔑舞。android.view.View.OnClickListener.onClick(..)
表示的執(zhí)行OnClickListener的onClick()方法拒担。onClick(..)
中的..
表示任意類型、任意個數(shù)的參數(shù)攻询。 - onClickMethodAround:表示的實際切入代碼澎蛛。這個方法名可以自己隨意定義。
在上面的例子中實際上我是自定義了一個PointCut蜕窿,名字是clickMethod()
谋逻。這個名稱隨意,只要在advice中指定好該名稱就可以了桐经。
@Pointcut("execution(* android.view.View.OnClickListener.onClick(..))")
public void clickMethod() {}
如果不想自定義毁兆,可以直接這樣:
@Around("execution(* android.view.View.OnClickListener.onClick(..))")
public void onClickMethodAround(ProceedingJoinPoint joinPoint) throws Throwable {
...
}
call和execution
我們之前講的切入點語法都是execution,那么如果使用call有什么區(qū)別呢阴挣?
我們再使用一個例子气堕,創(chuàng)建一個切面用來打印方法的執(zhí)行時間,并且只處理帶有注解的參數(shù)畔咧。
TimeSpend 注冊如下茎芭,value表示的是方法的功能
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface TimeSpend {
String value() default "";
}
使用execution打印方法執(zhí)行時間的切面如下:
@Aspect
public class MethodSpendTimeAspect {
private static final String TAG = "MethodSpendTimeAspect";
@Pointcut("execution(@com.sososeen09.aop_tech.aspect.TimeSpend * *(..))")
public void methodTime() {}
@Around("methodTime()")
public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = methodSignature.getDeclaringType().getSimpleName();
String methodName = methodSignature.getName();
String funName = methodSignature.getMethod().getAnnotation(TimeSpend.class).value();
//統(tǒng)計時間
long begin = System.currentTimeMillis();
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - begin;
Log.e(TAG, String.format("功能:%s,%s類的%s方法執(zhí)行了,用時%d ms", funName, className, methodName, duration));
return result;
}
}
原始Java文件如下:
public class LoginActivity extends AppCompatActivity {
...
@TimeSpend("登錄")
private void attemptLogin() {
StatusHolder.sHasLogin = true;
Toast.makeText(this, "登錄成功", Toast.LENGTH_SHORT).show();
finish();
}
}
編譯之后的.class文件:
public class LoginActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
...
super.onCreate(savedInstanceState);
mEmailSignInButton.setOnClickListener(new OnClickListener() {
public void onClick(View view) {
LoginActivity.this.attemptLogin();
}
});
}
@TimeSpend("登錄")
private void attemptLogin() {
JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
attemptLogin_aroundBody1$advice(this, var1, MethodSpendTimeAspect.aspectOf(), (ProceedingJoinPoint)var1);
}
static {
ajc$preClinit();
}
}
如果把execution該為call誓沸,在看一下編譯后的 .class 文件 :
public class LoginActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
mEmailSignInButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View view) {
LoginActivity.access$000(com.sososeen09.aop_tech.LoginActivity.this);
}
});
}
@TimeSpend("登錄")
private void attemptLogin() {
StatusHolder.sHasLogin = true;
Toast.makeText(this, "登錄成功", 0).show();
this.finish();
}
static {
ajc$preClinit();
}
static void access$000(LoginActivity x0) {
JoinPoint makeJP = Factory.makeJP(ajc$tjp_0, null, x0);
attemptLogin_aroundBody1$advice(x0, makeJP, MethodSpendTimeAspect.aspectOf(), (ProceedingJoinPoint) makeJP);
}
}
看到區(qū)別了吧梅桩,execution表示JPoint是執(zhí)行方法的地方,AspectJ會對被執(zhí)行方法做處理拜隧。而call表示JPoint是調(diào)用方法的地方宿百,AspectJ會對調(diào)用處做處理趁仙。
總結(jié)
本文介紹了AOP的一些概念性的知識,簡單介紹了AspectJ在Android開發(fā)中的基本使用方式垦页。限于篇幅和水平雀费,難以對AspectJ做一個全面的介紹,建議對AOP和AspectJ有興趣的讀者可以閱讀下面的相關(guān)項目和文章痊焊,也歡迎交流盏袄。