百度 攻略使用技巧,AspectJ在Android 中的使用攻略

 2023-10-22 阅读 25 评论 0

摘要:作者 | 唯鹿? ? ? ?責編 | 歐陽姝黎出品 | CSDN博客AOP(aspect-oriented programming),指的是面向切面編程。而AspectJ是實現AOP的其中一款框架,內部通過處理字節碼實現代碼注入。AspectJ從2001年發展至今,已經非常成熟穩定,同時使

作者 | 唯鹿? ? ? ?責編 | 歐陽姝黎

出品 | 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中代表不定參數的意思

Advice

用來指定代碼插入到Pointcuts的什么位置。

After、Before 示例

這里我們實現一個功能,在所有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示例

當方法執行出現異常,且異常沒有處理時,可以使用@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。

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

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 年傳奇人生畫下句號

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/2/159509.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息