作者 | 唯鹿? ? ? ?責編 | 歐陽姝黎
出品 | CSDN博客
AOP(aspect-oriented programming),指的是面向切面編程。而AspectJ是實現AOP的其中一款框架,內部通過處理字節碼實現代碼注入。
AspectJ從2001年發展至今,已經非常成熟穩定,同時使用簡單是它的一大優點。至于它的使用場景,可以看本文中的一些小例子,獲取能給你啟發。
集成AspectJ
使用插件gradle-android-aspectj-plugin
這種方式接入簡單。但是此插件截止目前已經一年多沒有維護了,考慮到AGP的兼容性,害怕以后無法使用。這里就不推薦了。(這里存在特殊情況,文章后面會提到。)
常規的Gradle 配置方式
百度 攻略使用技巧?這種方法相對配置會多一些,但相對可控。
首先在項目根目錄的build.gradle中添加:
classpath "com.android.tools.build:gradle:4.2.1"
classpath?'org.aspectj:aspectjtools:1.9.6'
然后在app的build.gradle中添加:
dependencies {...implementation 'org.aspectj:aspectjrt:1.9.6'
}import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Mainfinal def log = project.logger
final def variants = project.android.applicationVariantsvariants.all { variant ->// 注意這里控制debug下生效,可以自行控制是否生效if (!variant.buildType.isDebuggable()) {log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")return}JavaCompile javaCompile = variant.javaCompileProvider.get()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.thrownbreakcase IMessage.WARNING:log.warn message.message, message.thrownbreakcase IMessage.INFO:log.info message.message, message.thrownbreakcase IMessage.DEBUG:log.debug message.message, message.thrownbreak}}}
}
在 module 使用的話一樣需要添加配置代碼(略有不同):
dependencies {...implementation 'org.aspectj:aspectjrt:1.9.6'}import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Mainfinal def log = project.loggerandroid.libraryVariants.all{ variant ->if (!variant.buildType.isDebuggable()) {log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")return}JavaCompile javaCompile = variant.javaCompileProvider.get()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.thrownbreakcase IMessage.WARNING:log.warn message.message, message.thrownbreakcase IMessage.INFO:log.info message.message, message.thrownbreakcase IMessage.DEBUG:log.debug message.message, message.thrownbreak}}}
}
AspectJ基礎語法
Join Points
連接點,用來連接我們需要操作的位置。比如連接普通方法、構造方法還是靜態初始化塊等位置,以及是調用方法外部還是調用方法內部。常用類型有Method call、Method execution、Constructor call、Constructor execution等。
Android不使用布局文件,Pointcuts
切入點,是帶條件的Join Points,確定切入點位置。
execution和call的區別如下圖:
Pattern規則如下:
上表中中括號為可選項,沒有可以不寫
方法匹配例子:
1) java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
2) Test*:可以表示TestBase,也可以表示TestDervied
3) java..*:表示java任意子類
4) java..*Model+:表示Java任意package中名字以Model結尾的子類,比如TabelModel,TreeModel 等 ?
耀 攻略、參數匹配例子:
1) (int, char):表示參數只有兩個,并且第一個參數類型是int,第二個參數類型是char
2) (String, ..):表示至少有一個參數。并且第一個參數類型是String,后面參數類型不限.
3) ..代表任意參數個數和類型
4) (Object ...):表示不定個數的參數,且類型都是Object,這里的...不是通配符,而是Java中代表不定參數的意思
用來指定代碼插入到Pointcuts的什么位置。
這里我們實現一個功能,在所有Activity的onCreate方法中添加Trace方法,來統計onCreate方法耗時。
@Aspect // <-注意添加,才會生效參與編譯
public class TraceTagAspectj {@Before("execution(* android.app.Activity+.onCreate(..))")public void before(JoinPoint joinPoint) {Trace.beginSection(joinPoint.getSignature().toString());}@After("execution(* android.app.Activity+.onCreate(..))")public void after() {Trace.endSection();}
}
編譯后的class代碼如下:
可以看到經過處理后,它并不會直接把 Trace 函數直接插入到代碼中,而是經過一系列自己的封裝。如果想針對所有的函數都做插樁,AspectJ 會帶來不少的性能影響。
Android10、不過大部分情況,我們可能只會插樁某一小部分函數,這樣 AspectJ 帶來的性能影響就可以忽略不計了。
AfterReturning示例
獲取切點的返回值,比如這里我們獲取TextView,打印它的text值。
private TextView testAfterReturning() {return findViewById(R.id.tv);
}
@Aspect
public class TextViewAspectj {@AfterReturning(pointcut = "execution(* *..*.testAfterReturning())", returning = "textView") // "textView"必須和下面參數名稱一樣public void getTextView(TextView textView) {Log.d("weilu", "text--->" + textView.getText().toString());}
}
編譯后的class代碼如下:
log打印:
借唄攻略。使用@AfterReturning你可以對方法的返回結果做一些修改(注意是“=”賦值,String無法通過此方法修改)。
當方法執行出現異常,且異常沒有處理時,可以使用@AfterThrowing。比如下面的例子中,我們捕獲異常并上報(這里用log輸出實現)
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);testAfterThrowing();}private void testAfterThrowing() {TextView textView = null;textView.setText("aspectj");}
}
@Aspect
public class ReportExceptionAspectj {@AfterThrowing(pointcut = "call(* *..*.testAfterThrowing())", throwing = "throwable") // "throwable"必須和下面參數名稱一樣public void reportException(Throwable throwable) {Log.e("weilu", "throwable--->" + throwable);}
}
log打印
這里要注意的是,程序最終還是會崩潰,因為最后執行了throw var3。如果你想不崩潰,可以使用@Around。
接著上面的例子,我們這次直接try catch住異常代碼:
@Aspect
public class TryCatchAspectj {@Pointcut("execution(* *..*.testAround())")public void methodTryCatch() {}@Around("methodTryCatch()")public void aroundTryJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {try {joinPoint.proceed(); // <- 調用原代碼} catch (Exception e) {e.printStackTrace();}}
}
Android?編譯后的class代碼如下:
@Around 明顯更加靈活,我們可以自定義,實現"偷梁換柱"的效果,比如上面提到的替換方法的返回值。
withincode表示某個方法執行過程中涉及到的JPoint,通常用來過濾切點。例如我們有一個Person對象:
public class Person {private String name;private int age;public Person() {this.name = "weilu";this.age = 18;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getAge() {return age;}public void setAge(int age) {this.age = age;}
}
Person對象中有兩處set age的地方,如果我們只想讓構造方法的生效,讓setAge方法失效,可以使用@Around("execution(* com.weilu.aspectj.demo.Person.setAge(..))")不過如果有更多處set age的地方,我們這樣一個個去匹配就很麻煩。
這里就可以考慮使用set這個Pointcuts:
public class FieldAspectJ {@Around("set(int com.weilu.aspectj.demo.Person.age)")
public void aroundFieldSet(ProceedingJoinPoint joinPoint) throws Throwable {Log.e("weilu", "around->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());}
}
由于set(FieldPattern)的FieldPattern限制,不能指定參數,這樣會將所有的set age都切入:
這時就可以使用withincode添加過濾條件:
@Aspect
public class FieldAspectJ {@Pointcut("!withincode(com.weilu.aspectj.demo.Person.new())")
public void invokePerson() {}@Around("set(int com.weilu.aspectj.demo.Person.age) && invokePerson()")
public void aroundFieldSet(ProceedingJoinPoint joinPoint) throws Throwable {Log.e("weilu", "around->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());}
}
Android使用自帶文件?結果如下:
還有一個within,它和withincode類似。不同的是,它的范圍是類,而withincode是方法。例如:within(com.weilu.activity.*)表示此包下任意的JPoint。
args
用來指定當前執行方法的參數條件。比如上一個例子中,如果需要指定第一個參數是int,后面參數不限。就可以這樣寫。
@Around("execution(*?com.weilu.aspectj.withincode.Person.setAge(..))?&&?args(int,..)")
cflow
cflow是call flow的意思,cflow的條件是一個pointcut
您正在使用Android、舉一個例子來說明一下它的用途,a方法中調用了b、c、d方法。此時要統計各個方法的耗時,如果按之前掌握的語法,我們最多需要寫四個Pointcut,方法越多越麻煩。
使用cflow,我們可以方便的掌握方法的“調用流”。我們測試方法如下:
private void test() {testAfterReturning();testAround();testWithInCode();
}
實現如下:
@Aspect
public class TimingAspect {@Around("execution(* *(..)) && cflow(execution(* com.weilu.aspectj.demo.MainActivity.test(..)))")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = currentTimeMillis();Object result = joinPoint.proceed();
long endTime = currentTimeMillis();Log.e("weilu", joinPoint.getSignature().toString() + " -> " + (endTime - startTime) + " ms");
return result;}}
cflow(execution(* com.weilu.aspectj.demo.MainActivity.test(..)))表示調用test方法時所包含的JPoint,包括自身JPoint。
execution(* *(..))的作用是去除TimingAspect自身的代碼,避免自己攔截自己,形成死循環。
log結果如下:
Android版本10?
還有一個cflowbelow,它和cflow類似。不同的是,它不包括自身JPoint。也就是例子中不會獲取test方法的耗時。
實戰
攔截點擊
攔截點擊的目的是避免因快速點擊控件,導致重復執行點擊事件。例如打開多次頁面,彈出多次彈框,請求多次接口,我之前發現在部分機型上,很容易復現此類情況。所以避免抖動這算是項目中的一個常見需求。
例如butterknife中就自帶DebouncingOnClickListener來避免此類問題。
Android中使用sdl、如果你已不在使用butterknife,也可以復制這段代碼。一個個的替換已有的View.OnClickListener。還有以前使用Rxjava操作符來處理防抖。但這些方式侵入式大且替換的工作量也大。
這種場景就可以考慮AOP的方式處理。攔截onClick方法,判斷是否可以點擊。
@Aspect
public class InterceptClickAspectJ {// 最后一次點擊的時間
private Long lastTime = 0L;
// 點擊間隔時長
private static final Long INTERVAL = 300L;@Around("execution(* android.view.View.OnClickListener.onClick(..))")
public void clickIntercept(ProceedingJoinPoint joinPoint) throws Throwable {
// 大于間隔時間可點擊
if (System.currentTimeMillis() - lastTime >= INTERVAL) {
// 記錄點擊時間lastTime = System.currentTimeMillis();
// 執行點擊事件joinPoint.proceed();} else {Log.e("weilu", "重復點擊");}}}
實現代碼很簡單,效果如下:
考慮到有些view的點擊事件不需要防抖,例如checkBox。否則checkBox狀態變了,但事件沒有執行。我們可以定義一個注解,用withincode過濾有此注解的方法。具體需求可以根據實際項目自行拓展,這里僅提供思路。
埋點
前面的例子中都是無侵入的方式使用AspectJ。這里說一下侵入式的方式,簡單說就是使用自定義注解,用注解作為切入點的規則。(其實也可以自定義一種方法命名,來當做切入規則)
android調用activity方法、首先定義兩個注解,一個用來傳固定參數比如eventName、eventId,同時負責當做切入點,一個用來傳動態參數的key。
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackEvent {/*** 事件名稱*/String eventName() default "";/*** 事件id*/String eventId() default "";
}@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackParameter {String value() default "";}
Aspectj代碼如下:
@Aspect
public class TrackEventAspectj {@Around("execution(@com.weilu.aspectj.tracking.TrackEvent * *(..))")public void trackEvent(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature signature = (MethodSignature) joinPoint.getSignature();// 獲取方法上的注解TrackEvent trackEvent = signature.getMethod().getAnnotation(TrackEvent.class);String eventName = trackEvent.eventName();String eventId = trackEvent.eventId();JSONObject params = new JSONObject();params.put("eventName", eventName);params.put("eventId", eventId);// 獲取方法參數的注解Annotation[][] parameterAnnotations = signature.getMethod().getParameterAnnotations();if (parameterAnnotations.length != 0) {int i = 0;for (Annotation[] parameterAnnotation : parameterAnnotations) {for (Annotation annotation : parameterAnnotation) {if (annotation instanceof TrackParameter) {// 獲取key valueString key = ((TrackParameter) annotation).value();params.put(key, joinPoint.getArgs()[i++]);}}}}// 上報Log.e("weilu", "上報數據---->" + params.toString());try {joinPoint.proceed();} catch (Throwable throwable) {throwable.printStackTrace();}}
}
使用方法:
@TrackEvent(eventName = "點擊按鈕", eventId = "100")
private void trackMethod(@TrackParameter("uid") int uid, String name) {Intent intent = new Intent(this, KotlinActivity.class);intent.putExtra("uid", uid);intent.putExtra("name", name);startActivity(intent);}trackMethod(10,?"weilu");
結果如下:
由于匹配key value的代碼問題,建議將需要動態傳入的參數都寫在前面,避免下標越界。
由于匹配key value的代碼問題,建議將需要動態傳入的參數都寫在前面,避免下標越界。
android studio獲取輸入框的內容。還有一些使用場景,比如權限控制。總結一下,AOP適合將一些通用邏輯分離出來,然后通過AOP將此部分注入到業務代碼中。這樣我們可以更加注重業務的實現,代碼也顯得清晰起來。
其他問題
lambda
如果我們代碼中有使用lambda,例如點擊事件會變為:
tv.setOnClickListener(v -> Log.e("weilu", "點擊事件執行"));
這樣之前的點擊切入點就無效了,這里涉及到D8這個脫糖工具和invokedynamic字節碼指令相關知識,這里我也無法說的清楚詳細。簡單說使用lambda會生成lambda$開頭的中間方法,所以只能如下處理:
@Around("execution(* *..lambda$*(android.view.View))")
這種暫時處理起來比較麻煩,且可以看出容錯率也比較低,很容易切入其他無關方法,所以建議AOP不要使用lambda。
配置
android自定義view的三大流程?一開始介紹了兩種配置,雖說AspectJX插件最近不太維護了,但是它的支持了AAR、JAR及Kotlin的切入,而默認僅是對自己的代碼進行切入。
在AspectJ常規配置中有這樣的代碼:"-inpath", javaCompile.destinationDir.toString(),代表只對源文件進行織入。在查看Aspectjx源碼時,發現在“-inputs”配置加入了.jar文件,使得class類可以被織入代碼。這么理解來看,AspectJ也是支持對class文件的織入的,只是需要對它進行相關的配置,而配置比較繁瑣,所以誕生了AspectJx等插件。
例如Kotlin在需要在常規的Gradle 配置上增加如下配置:
def buildType = variant.buildType.name
String[] kotlinArgs = [
"-showWeaveInfo",
"-1.8",
"-inpath", project.buildDir.path + "/tmp/kotlin-classes/" + buildType,
"-aspectpath", javaCompile.classpath.asPath,
"-d", project.buildDir.path + "/tmp/kotlin-classes/" + buildType,
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
MessageHandler handler = new MessageHandler(true)
new?Main().run(kotlinArgs,?handler)
同時注意用kotlin寫對應的Aspect類,畢竟你需要注入的是kotlin代碼,用java的肯定不行,但是反過來卻可行。
建議有AAR、JAR及Kotlin需求的使用插件方式,即使后期無人維護,可自行修改源碼適配GAP,相對難度不大。
這部分內容較多同時也比較枯燥,斷斷續續整理了一周的時間。基本介紹了AspectJ在Android 中的配置,以及常用的語法與使用場景。對于應用AspectJ來說夠用了。
Android 10正式版,最后本篇涉及的代碼都已上傳至Github,有興趣的同學可以用做參考。
參考
AOP之AspectJ在Android中的應用
AOP 之 AspectJ 全面剖析 in Android
編譯插樁的三種方法:AspectJ、ASM、ReDex
Android 引入AspectJ的記錄
?Windows 11 正式官宣:全新 UI、支持安卓 App、應用商店 0 抽成!?React 毀了 Web 開發!
?殺毒軟件 McAfee 創始人獄中身亡,75 年傳奇人生畫下句號
版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。
工作时间:8:00-18:00
客服电话
电子邮件
admin@qq.com
扫码二维码
获取最新动态