最近有做用戶行為統計的需求,為了盡可能使統計代碼不侵入業務代碼,就研究了下hook和Aop。研究了下AspectJ,雖然還是不能完美解決項目中的問題,不過確實是個好東西。
編譯插樁是什么
顧名思義,所謂編譯插樁就是在代碼編譯期間修改已有的代碼或者生成新代碼。實際上,我們項目中經常用到的 Dagger、ButterKnife 甚至是 Kotlin 語言,它們都用到了編譯插樁的技術。
理解編譯插樁之前,需要先回顧一下 Android 項目中 .java 文件的編譯過程
從上圖可以看出,我們可以在 1、2 兩處對代碼進行改造。
在 .java 文件編譯成 .class 文件時,APT、AndroidAnnotation 等就是在此處觸發代碼生成。
在 .class 文件進一步優化成 .dex 文件時,也就是直接操作字節碼文件,也是本課時主要介紹的內容。這種方式功能更加強大,應用場景也更多。但是門檻比較高,需要對字節碼有一定的理解。
本課時主要介紹第 1種實現方式
一般情況下,我們經常會使用編譯插樁實現如下幾種功能:
- 日志埋點;
- 性能監控;
- 動態權限控制;
- 業務邏輯跳轉時,校驗是否已經登錄;
- 甚至是代碼調試等。
常見AOP編程庫
在Java中,常見的面向切面編程的開源庫有:
AspectJ:和Java語言無縫銜接的面向切面的編程的擴展工具(可用于Android)。
Javassist for Android:一個移植到Android平臺的非常知名的操縱字節碼的java庫。
DexMaker:用于在Dalvik VM編譯時或運行時生成代碼的基于java語言的一套API。
ASMDEX:一個字節碼操作庫(ASM),但它處理Android可執行文件(DEX字節碼)。
Android集成AspectJ ,主要有兩種方式:
1,插件的方式:網上有人在github上提供了集成的插件gradle-android-aspectj-plugin。這種方式配置簡單方便,但經測試無法兼容databinding框架。
2,Gradle配置的方式:配置有點麻煩,不過國外一個大牛在build文件中添加了一些腳本,雖然有點難懂,但可以在AS中使用文章出處
方式一集成
首先,新建一個AS原工程,然后再創建一個aspectJLib module(Android Library) 。
項目根目錄build文件添加
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
項目 aspectJLib module里面 的build文件里面添加
apply plugin: 'com.android.library'
apply plugin: 'android-aspectjx' //添加代碼
android {
def version = rootProject.ext
compileSdkVersion version.compileSdkVersion
defaultConfig {
minSdkVersion version.minSdkVersion
targetSdkVersion version.targetSdkVersion
versionCode version.versionCode
versionName version.versionName
}
}
dependencies {
compile 'org.aspectj:aspectjrt:1.8.7'//添加代碼
}
然后在使用的app module 里面的build 文件添加插件
apply plugin: 'android-aspectjx'
方式二集成
參考文章
主要是編寫build腳本,添加任務,使得IDE使用ajc作為編譯器編譯代碼,然后把該Module添加至主工程Module中。
項目根build目錄添加
classpath 'org.aspectj:aspectjtools:1.8.1'
項目 aspectJLib module里面 的build文件里面添加
import com.android.build.gradle.LibraryPlugin
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
apply plugin: 'android-library'
android {
compileSdkVersion 19
buildToolsVersion '19.1.0'
lintOptions {
abortOnError false
}
}
dependencies {
compile 'org.aspectj:aspectjrt:1.8.1'
}
//編寫build腳本,添加任務
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;
}
}
}
}
然后在主build.gradle(Module:app)中添加也要添加AspectJ依賴,同時編寫build腳本,添加任務,目的就是為了建立兩者的通信,使得IDE使用ajc編譯代碼。
apply plugin: 'com.android.application'import org.aspectj.bridge.IMessageimport org.aspectj.bridge.MessageHandlerimport org.aspectj.tools.ajc.Main
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.1' }
}
repositories {
mavenCentral()
}
android {
compileSdkVersion 21 buildToolsVersion '21.1.2'
defaultConfig {
applicationId 'com.example.myaspectjapplication' minSdkVersion 15 targetSdkVersion 21 }
lintOptions {
abortOnError true }
}
final def log = project.loggerfinal 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.5",
"-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;
}
}
}
}
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
compile project(':aspectJLib')
compile 'org.aspectj:aspectjrt:1.8.1'
}
需要注意的是,由于不同版本的gradle在獲取編譯時獲取類的路徑等信息Api不同,所以以上groovy配置語句僅在Gradle Version高于3.3的版本上生效。
開始使用 本例子是按照方式一集成的
AspectJ 的兩種用法
(1)用自定義注解修飾切入點,精確控制切入點,屬于侵入式;
(2)不需要在切入點代碼中做任何修改,屬于非侵入式。
- 侵入式
侵入式用法,一般會使用自定義注解,以此作為選擇切入點的規則。 - 非侵入式
非侵入式,就是不需要使用額外的注解來修飾切入點,不用修改切入點的代碼。
侵入式
新增注解
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface DebugTrace {
}
新建一個Java類Aspect
打上@Aspect注解,則該類可以被ajc編譯器識別為一個Asepct,在工程項目編譯時便能非常方便地實現代碼織入??吹紸spectJ的三個要素,Join Point、Advice和Aspect。好像少了Join Point?Join Point早已定義在Pointcut的字符串常量中(即execution),即MainActivity的onCreate方法。Pointcut以注解的形式定義,注解了timeWatch方法,從而timeWatch就是這個Pointcut的名稱,注解參數則使用定義好的字符串常量,作為Join Point的過濾規則。同樣,Advice也是將類型關鍵字(此處為Around)注解在特定的方法saveJoinPoint之上,注解的參數為具名的Pointcut,即timeWatch。上文提到Around類型即用該方法替換原Join Point的實現,Object result = joinPoint.proceed()等價于原有的被Hook方法,即MainActivity的onCreate()。在該語句的前后,是性能統計的代碼片段。
/**
* Aspect representing the cross cutting-concern: Method and Constructor Tracing.
* 侵入式的編譯注解
* 侵入式用法,一般會使用自定義注解,以此作為選擇切入點的 Pointcut 規則。
*/
@Aspect
public class TraceAspect {
/**
* 針對所有繼承 Activity 類的 onCreate 方法
*/
@Pointcut("execution(* android.app.Activity+.onCreate(..))")
public void activityOnCreatePointcut() {
}
//被"org.android10.gintonic.annotation.DebugTrace"標記的方法。
//針對帶有DebugTrace注解的方法
@Pointcut("execution(@org.android10.gintonic.annotation.DebugTrace * *(..))")
public void methodAnnotatedWithDebugTrace() {
}
//被"org.android10.gintonic.annotation.DebugTrace"標記的構造器。
@Pointcut("execution(@org.android10.gintonic.annotation.DebugTrace *.new(..))")
public void constructorAnnotatedDebugTrace() {
}
/**
* 我們定義的 "weaveJointPoint(ProceedingJoinPoint joinPoint)"
* 這個方法被添加了"@Around"注解,這意味著我們的代碼注入將發生在被
* "@DebugTrace"注解標記的方法前后。
* 在用DebugTrace注解修飾的方法或者構造函數里面注入如下代碼。
*/
@Around("methodAnnotatedWithDebugTrace() || constructorAnnotatedDebugTrace() || activityOnCreatePointcut()")
public Object saveJoinPoint(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();
Log.d("TAG", 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();
}
}
添加注解檢測運行時間
@DebugTrace
private void testAnnotatedMethod() {
SystemClock.sleep(100);
}
@DebugTrace
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_relative_layout_test);
}
此時運行代碼 便可以看到已經打印了時間
D/TAG: MainActivity--Gintonic --> testAnnotatedMethod --> [100ms]
D/TAG: MainActivity--Gintonic --> onCreate --> [187ms]
源代碼與反編譯后的代碼
反編譯項目生成的apk后可以看到,ajc在Join Point處織入了代碼,用TimeWatchAspect.aspectOf().saveJoinPoint()實現了替換。
//源代碼
@DebugTrace
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_relative_layout_test);
}
//反編譯后的代碼
protected void onCreate(Bundle paramBundle)
{
JoinPoint localJoinPoint = Factory.makeJP(ajc$tjp_0, this, this, paramBundle);
TimeWatchAspect.aspectOf().saveJoinPoint(new MainActivity.AjcClosure1(new Object[] { this, paramBundle, localJoinPoint }).linkClosureAndJoinPoint(69648));
}
public class MainActivity$AjcClosure1 extends AroundClosure
{
public MainActivity$AjcClosure1(Object[] paramArrayOfObject)
{
super(paramArrayOfObject);
}
public Object run(Object[] paramArrayOfObject)
{
Object[] arrayOfObject = this.state;
MainActivity.onCreate_aroundBody0((MainActivity)arrayOfObject[0], (Bundle)arrayOfObject[1], (JoinPoint)arrayOfObject[2]);
return null;
}
}
非侵入式
檢測View 的點擊花事件
import android.util.Log;
import android.view.View;
import org.android10.gintonic.TrackPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
/**
* @author wangwei
* @date 2021/4/12.
* 非侵入式 檢測onClick
* 非侵入式,就是不需要使用額外的注解來修飾切入點,不用修改切入點的代碼。
*/
@Aspect
public class ViewAspect {
@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
public void onClickPointcut() {
}
@Around("onClickPointcut()")
public void aroundJoinClickPoint(final ProceedingJoinPoint joinPoint) throws Throwable {
Object target = joinPoint.getTarget();
String className = "";
if (target != null) {
className = target.getClass().getName();
}
//獲取點擊事件view對象及名稱,可以對不同按鈕的點擊事件進行統計
Object[] args = joinPoint.getArgs();
if (args.length >= 1 && args[0] instanceof View) {
View view = (View) args[0];
int id = view.getId();
String entryName = view.getResources().getResourceEntryName(id);
//獲取點擊事件對不同按鈕的點擊事件進行統計
TrackPoint.onClick(className, entryName);
}
joinPoint.proceed();//執行原來的代碼
}
}
在Activity的所有生命周期的方法中打印log
/**
*
* @param joinPoint
* @throws Throwable
*/
@Before("execution(* android.app.Activity.**(..))")
public void method(JoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String className = joinPoint.getThis().getClass().getSimpleName();
Log.e("TAG", "class:" + className + " method:" + methodSignature.getName());
}
點擊一個按鈕可以看到打印日志
onClick: org.android10.viewgroupperformance.activity.MainActivity$1-btnRelativeLayout
原理及其重點
AOP的目標是把這些功能集中起來,放到一個統一的地方來控制和管理。如果說,OOP如果是把問題劃分到單個模塊的話,那么AOP就是把涉及到眾多模塊的某一類問題進行統一管理。比如我們可以設計兩個Aspects,一個是管理某個軟件中所有模塊的日志輸出的功能,另外一個是管理該軟件中一些特殊函數調用的權限檢查。
非侵入式監控 可以在不修監控目標的情況下監控其運行截獲某類方法甚至可以修改其參數和運行軌跡
基本原理
橫向的切割某一類方法和屬性,我們不需要顯式的修改就可以向代碼中添加可執行的代碼塊
它在編譯期將開發者編寫的Aspect程序編織到目標程序(PointCut定義的位置)中,對目標程序作了重構,以達到非侵入代碼監控的目的
編寫Aspect聲明Aspect、PointCut和Advise。
ajc編織 AspectJ編譯器在編譯期間對所切點所在的目標類進行了重構在編譯層將AspectJ程序與目標程序進行雙向關聯生成新的目標字節碼即將AspectJ的切點和其余輔助的信息類段插入目標方法和目標類中同時也傳回了目標類以及其實例引用。這樣便能夠在AspectJ程序里對目標程序進行監聽甚至操控。
AspectJ概念
AspectJ向Java引入了一個新的概念:join point,它包括幾個新的結構: pointcuts,advice,inter-type declarations 和 aspects。
一些概念詳解:
Pointcut:告訴代碼注入工具,在何處注入一段特定代碼的表達式。切點分為execution方式和annotation方式。前者可以用路徑表達式指定哪些類織入切面,后者可以指定被哪些注解修飾的代碼織入切面。
Advice:如何注入到我的class文件中的代碼。典型的 Advice 類型有 before、after 和 around,分別表示在目標方法執行之前、執行后和完全替代目標方法執行的代碼。
Aspect:Pointcut 和 Advice 的組合看做切面。例如,我們在應用中通過定義一個 pointcut 和給定恰當的advice,添加一個日志切面。
Join Points,簡稱JPoints,是AspectJ的核心思想之一,它就像一把刀,把程序的整個執行過程切成了一段段不同的部分。例如,構造方法調用、調用方法、方法執行、異常等等,這些都是Join Points,實際上,也就是你想把新的代碼插在程序的哪個地方,是插在構造方法中,還是插在某個方法調用前,或者是插在某個方法中,這個地方就是Join Points,當然,不是所有地方都能給你插的,只有能插的地方,才叫Join Points。
————————————————
注意 @Around 環繞通知 里面是ProceedingJoinPoint 可用proceed()調用自身方法
Weaving:織入,就是通過動態代理,在目標對象方法中執行處理內容的過程。
網絡上有張圖,我覺得非常傳神,貼在這里供大家觀詳:
execution表達式
我們使用最多的就是execution表示了,下面就從execution表達式開始介紹吧。
開發中常用到的pointCut 解釋:更多使用方式可參考底部鏈接 Pointcut語法詳解
返回值為void的點擊事件 參數為任意類型
@Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
androidx.appcompat.app.AppCompatActivity包下AppCompatActivity類及子類型的任何方法 參數為任意類型
@Pointcut("execution(* androidx.appcompat.app.AppCompatActivity+.*(..))")
任何持有com.aspectj.lib.annotation.DebugTrace注解的方法
@Pointcut("execution(@com.aspectj.lib.annotation.DebugTrace * *(..))")
定義任何一個以"set"開始的方法的執行
@Pointcut(execution(* set*(..)) )
定義AccountService 的任意方法的執行
@Pointcut(execution(* com.xyz.service.AccountService.*(..)))
定義在service包里的任意類名的任意方法的執行
@Pointcut(execution(* com.xyz.service.*.*(..)))
定義在service包和所有子包里的任意類的任意方法的執行
@Pointcut(execution(* com.xyz.service ..*.*(..)))
在MainActivity 且自帶有一個String 參數的方法
@Pointcut( "execution(* com.aspectj.example.activity.MainActivity.*(..)) && args(java.lang.String)) ";
execution(
modifier-pattern? 修飾符部分 例如 public private...
ret-type-pattern 返回值部分 例如 return String;
declaring-type-pattern? 描述包名 例如 cn.evchar....
name-pattern(param-pattern) 描述方法名,描述方法參數
throws-pattern? 匹配拋出的異常
)
修飾符是可以省略的 ,返回值類型就是普通的函數的返回值類型。如果不限定類型的話就用*通配符表示
說明:最靠近(..)的為方法名,靠近.?(..))的為類名或者接口名
如:
例如定義切入點表達式 execution(* com.sample.service.impl..*.*(..))
execution()是最常用的切點函數,其語法如下所示:
整個表達式可以分為五個部分:
1、execution(): 表達式主體。
2、第一個*號:表示返回類型,*號表示所有的類型。
3、包名:表示需要攔截的包名,后面的兩個句點表示當前包和當前包的所有子包,com.sample.service.impl包、子孫包下所有類的方法。
4、第二個*號:表示類名,*號表示所有的類。
5、*(..):最后這個星號表示方法名,*號表示所有的方法,后面括弧里面表示方法的參數,兩個句點表示任何參數。
通配符意思:
..* :表示包、子孫包下的所有類
.* :表示包下的所有類
* :匹配任何數量字符;
.. :匹配任何數量字符的重復,如在類型模式中匹配任何數量子包;而在方法參數模式中匹配任何數量參數。
+ :匹配指定類型的子類型;僅能作為后綴放在類型模式后邊。
比如:
java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
Test*:可以表示TestBase,也可以表示TestDervied
java..*:表示java任意子類
java..*Model+:表示Java任意package中名字以Model結尾的子類,比如TabelModel,TreeModel等
函數的參數,參數匹配比較簡單,主要是參數類型,比如:
(int, char):表示參數只有兩個,并且第一個參數類型是int,第二個參數類型是char
(String, ..):表示至少有一個參數。并且第一個參數類型是String,后面參數類型不限。在參數匹配中,
(..) 代表任意參數個數和類型
(Object ...):表示不定個數的參數,且類型都是Object,這里的...不是通配符,而是Java中代表不定參數的意思
Spring AOP支持的AspectJ切入點指示符如下:由下列方式來定義或者通過 &&、 ||、 !、 的方式進行組合: 如:@Around(value= "methodPointcut2() && (args(request, ..) || args(.., request))")
- execution:用于匹配方法執行的連接點;
("execution(* com.zx.aop1.Person.eat())") 精確地匹配到Person類里的eat()方法
- call是在調用被切入的方法前或者后, execution是在被切入的方法中。
//對于Call來說:
Call(Before)
Pointcut{
Pointcut Method
}
Call(After)
//對于Execution來說:
Pointcut{
execution(Before)
Pointcut Method
execution(After)
}
within:用于匹配指定類型內的方法執行;
within 是用來指定類型的 within的粒度為類 如:!within(androidx.appcompat.app.AppCompatActivity) AppCompatActivity類里面的所有都會被攔截
this:用于匹配當前AOP代理對象類型的執行方法;注意是AOP代理對象的類型匹配,這樣就可能包括引入接口也類型匹配;
target:用于匹配當前目標對象類型的執行方法;注意是目標對象的類型匹配,這樣就不包括引入接口也類型匹配;、
target與this總結:
1、target指代的是切點方法的所有者,而this指代的是被織入代碼所屬類的實例對象。
2、如果當前要代理的類沒有實現某個接口就用 this;如果實現了某個接口,就使用 target
target() 與 this() 很容易混淆,target() 是指 Pointcut 選取的 Join Point 的所有者;this() 是指 Pointcut 選取的 Join Point 的調用的所有者。簡單地說就是,PointcutA 選取的是 methodA,那么 target 就是 methodA() 這個方法的對象,而 this 就是 methodA 被調用時所在類的對象
args:用于匹配當前執行的方法傳入的參數為指定類型的執行方法;
args(java.lang.String) 有一個參數為String的方法
-
@within:用于匹配所以持有指定注解類型內的方法;
@within(注解類型) 匹配所有使用了CheckWithin注解的類(只能作用于類,不能是方法,也不能是接口。) @Pointcut("@within(com.zx.aop1.CheckWithin)")
@target:用于匹配當前目標對象類型的執行方法,其中目標對象持有指定的注解;
@args:用于匹配當前執行的方法傳入的參數持有指定注解的執行;
@annotation:用于匹配當前執行方法持有指定注解的方法;
匹配使用了CheckAop注解的方法(注意是方法) @Pointcut("@annotation(com.zx.aop1.CheckAop)")
AspectJ切入點支持的切入點指示符還有: call、get、set、preinitialization、staticinitialization、initialization、handler、adviceexecution、withincode、cflow、cflowbelow、if、@this、@withincode;但Spring AOP目前不支持這些指示符,使用這些指示符將拋出IllegalArgumentException異常。這些指示符Spring AOP可能會在以后進行擴展。
踩過的坑
由于AspectJ在字節碼層面將功能性代碼織入業務代碼中,源碼層面無法看到變化,且無法在功能性代碼中進行斷點調試。所以一旦出錯,調試成本相對較高。如果項目運行結果與預期不符,首先檢查編譯問題,能否正常實現代碼織入(可以看apk中的class文件樹結構),再檢查Join Point、Pointcut和Advice是否符合AspectJ語法,Hook是否正確。
如果Android Studio中的Instant Run開啟,則在編譯時可能會影響代碼的正常織入,所以建議關閉Instant Run。
另外,一般初級階段會選擇日志打印的方式驗證AspectJ接入的可行性。如果測試機是魅族系列手機,則注意把項目中Log等級提升到D以上,或者在手機的開發者選項中選擇顯示所有等級的日志,否則默認情況下你看不到D及D以下等級日志的輸出(慘痛的教訓,浪費了兩天時間排查問題)。
每次改變aspect代碼需要clean項目
Execution failed for task ':app:transformClassesWithDexBuilderForDebug'.
> com.android.build.api.transform.TransformException: java.util.zip.ZipException: zip file is empty
對應版本:
aspectjx:2.0.4
androidstudio3.2.1
android tools 3.2.1
gradle4.6
導致原因:
新寫的Pointcut有問題
緩存問題
解決方法:
修改用問題的Pointcut
清除app內的build文件
清除C:\Users\用戶名.AndroidStudio3.2.1\system\caches中的內容
參考文章
AOP原理1
Pointcut語法詳解
Pointcut 切面函數的過濾規則
深入理解Android之AOP
Android Studio 中自定義 Gradle 插件
看AspectJ在Android中的強勢插入
Aspect Oriented Programming in Android
AspectJX與第三方庫沖突的解決方案