加入引用
在整個項目的 build.gradle 中,添加?
classpath "com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10"
?可以看到測試demo的 gradle 版本是很低的。
基于 github 上的文檔,可以看到原版只支持到 gradle 4.4 。后續需要使用社區版的 aspectjx
GitHub - HujiangTechnology/gradle_plugin_android_aspectjx: A Android gradle plugin that effects AspectJ on Android project and can hook methods in Kotlin, aar and jar file.
然后在App 目錄下的 build.gradle?中加入plugin 標記即可。
apply plugin: 'android-aspectjx'
還可以指定需要生效的位置,塊放在最后即可。
include 生效的包名
exclude 排除的包名
使用
基于 @Aspect 注解,LogAspect? 不需要在任何地方調用,自動會織入。
package com.aaaa.testplayer;import android.util.Log;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;/*** 切入點學習:* 所有類切入點: (* com.aaaa.testplayer..*(..))* 1. execution(...):* 這是一個切點表達式,用于匹配方法執行的連接點。* <p>* 2. * (返回值類型):* 這個星號表示“任意返回類型”。也就是說,這個切點將匹配任何返回類型的方法,無論是 void、int、String 等。* <p>* 3. com.aaaa.testplayer..* (聲明類型模式):* com.aaaa.testplayer: 這是你的包名。* ..: 這個符號表示“零個或多個子包”。它允許在 com.aaaa.testplayer 包及其所有子包中匹配類。例如,它會匹配:* com.aaaa.testplayer.MyClass* com.aaaa.testplayer.subpackage.AnotherClass* *: 這個星號表示“任意類名”。它允許匹配該包下的所有類。* <p>* 4. ( .. ) (參數模式):* .. 代表“零個或多個參數”。這意味著該切點會匹配任何數量的參數,包括沒有參數的情況。比如:* myMethod()* myMethod(int a)* myMethod(String a, int b, double c)* <p>* <p>* 另一種實現:** @Pointcut("execution(* com.aaaa.testplayer.*.*(..))")* 匹配的是 com.example.service 包下的所有類和方法。* *.*(..) 的意思是匹配該包下任何類中的任何方法,不論返回類型、方法名稱或參數類型。*//**** JoinPoint :* 1. 獲取上下文信息:通過 JoinPoint,你可以獲取關于當前連接點的信息,比如方法名、類名、參數等等。* 2. 不能控制流程:JoinPoint 只能用來查看信息,而不能控制方法的執行。** ProceedingJoinPoint:* 1. 控制方法執行:你可以使用 proceed() 方法來繼續執行原始方法,或者選擇不執行它。* 2. 獲取上下文信息:像 JoinPoint 一樣,ProceedingJoinPoint 也可以訪問連接點的信息,但它還額外提供了一些可以控制的功能。*** JoinPoint 的使用,具體可以參考:beforeAdvice** ProceedingJoinPoint主要多了 proceed()方法,配合 @Around 環繞,可以控制方法是否執行** *//*** 無法動態修改已編譯的系統類!!!** 關于 throws Throwable 。可以使用 try/catch 組合來代替,主要取決于是否要向上傳遞 error* *//*** @Before: 在目標方法執行之前。* @After: 在目標方法執行之后,無論結果如何。* @AfterReturning: 在目標方法成功返回之后。* @AfterThrowing: 在目標方法拋出異常之后。* @Around: 可自定義目標方法的執行過程,包括前置和后置操作。* */
@Aspect
public class LogAspect {/*** @Before 在指定位置之前進行觸發* ************************************************************************/// 在指定的 class 方法運行前,進行log輸出
// @Before("execution(* com.aaaa.testplayer.MainActivity.onCreate(..))")
// public void beforeOnCreate() {
// Log.e("aaaaa","MainActivity onCreate called");
// }/*** 切入點學習:* 1. * android.util.Log.d(String, String):* * 表示“任意返回類型”,在這里它表示 Log.d 方法可以是任何返回類型(由于 Log.d 返回 int 作為日志行 ID,但我們通常不關心返回值)。** 2. android.util.Log.d: 這是我們要匹配的具體方法,即 Android 的 Log 類的 d 方法,表示 "debug" 日志。* (String, String): 表示該方法接受兩個 String 類型的參數。也就是說,它只會匹配傳遞兩個 String 參數的 Log.d 方法調用。** 3.&& args(tag, msg):* && 是邏輯與操作符,表示該切點表達式的兩個部分必須同時滿足。* args(tag, msg): 這個部分用于提取通過方法參數傳入的數據。tag 和 msg 是這兩個參數的名稱,可以在通知方法中使用。這允許你在切面中引用這些參數,使得你能夠對子日志信息進行處理或記錄。** 如果 不加 && args(tag, msg),那么下面就無法獲取整個參數!!!* */// 在指定的方法運行前,進行log輸出
// @Before("call(* android.util.Log.d(String, String)) && args(tag, msg)")
// public void replaceLog(String tag, String msg) {
// // 將所有 Log.d 前增加為 Log.e
// Log.e(tag, "[Replaced] " + msg);
// }// 在所有方法執行前被調用,并打印出正在執行的方法名稱。
// @Before("execution(* com.aaaa.testplayer..*(..))")
// public void beforeAdvice(JoinPoint joinPoint) {
// // 獲取方法名
// String methodName = joinPoint.getSignature().getName();
// // 可以用來獲得當前執行的方法所在的類的全名
// String methodDeclaringTypeName = joinPoint.getSignature().getDeclaringTypeName();
// // 用于在運行時獲取方法的聲明類對象,從而可以調用該類的方法、訪問其字段等。
// Class<?> declaringType = joinPoint.getSignature().getDeclaringType();
// // 注意這里因為劫持了所有方法所以要檢查 methodName, 不然會死循環
// if (declaringType == MainActivity.class && methodName == "onCreate") {
// MainActivity mainActivity = (MainActivity) joinPoint.getTarget(); // 獲取目標對象
// try {
// // 調用 showMsg 方法
// mainActivity.showMsg("This is a message from the aspect!");
// } catch (Exception e) {
// Log.e("AspectError", "Error calling showMsg method", e);
// }
// }
//
// // 獲取目標對象
// Object targetObject = joinPoint.getTarget();
//
// // 獲取代理對象
// Object proxyObject = joinPoint.getThis();
//
// // 獲取參數
// // 可以獲取到入參,例如在 public void onTest1(View view) 中可以獲取到入參 android.widget.Button{f2aba96 VFED..C.. .F....ID 0,0-300,90}
// Object[] args = joinPoint.getArgs();
//
// // 打印信息到 Logcat
// Log.e("aaaaa", "Method name: " + methodName);
// Log.e("aaaaa", "methodDeclaringTypeName name: " + methodDeclaringTypeName);
// Log.e("aaaaa", "目標對象 Target object: " + targetObject.getClass().getSimpleName());
// Log.e("aaaaa", "代理對象 Proxy object: " + proxyObject.getClass().getSimpleName());
// Log.e("aaaaa", "Arguments: " + Arrays.toString(args));
// if (args.length > 0) {
// for (Object arg : args) {
// if (arg == null){
// continue;
// }
// Log.e("Aspect", "Argument type: " + arg.getClass().getSimpleName() + ", value: " + arg);
// }
// }
//
// // 連接點描述
// Log.e("aaaaa", ("JoinPoint description: " + joinPoint.toString()));
//
// // 獲取源位置(文件名和行號)
// SourceLocation location = joinPoint.getSourceLocation();
// Log.e("aaaaa", "Source location: " + location.getFileName() + ":" + location.getLine());
// }/*** @Around 在運行中觸發* ************************************************************************//*** 1. @Around 通知* 允許你完全控制目標方法的執行,包括是否調用原方法(通過 joinPoint.proceed())。** 2.不調用 proceed()* 這里我們直接返回 Log.e 的結果,不執行 joinPoint.proceed(),從而完全跳過原始 Log.d 的執行。** 3.返回值處理* Log.d 和 Log.e 都返回 int(日志的優先級/類型),因此直接返回 Log.e 的返回值是類型安全的。如果調用代碼依賴返回值,也能保持一致。** 4.參數注入* 通過 args(tag, msg) 將原始參數注入方法,你可以在新邏輯中復用或修改它們。** @Around 在性能上與 @Before 差異不大,但避免了冗余的原方法調用。* */// 將全局的log.d 調用替換為 Log.e@Around("call(* android.util.Log.d(String, String)) && args(tag, msg)")public Object replaceLog(ProceedingJoinPoint joinPoint, String tag, String msg) throws Throwable {// 方法執行前的邏輯
// System.out.println("Before method: " + joinPoint.getSignature().getName());// 這里決定了是否要調用原有的參數。如果調用這句,約等于前面的 before ,不過會先打出log.d
// joinPoint.proceed();// 方法執行后的邏輯
// System.out.println("After method: " + joinPoint.getSignature().getName());// 直接調用 Log.e 替換 Log.d,并阻止原方法執行return Log.e(tag, "[Replaced] " + msg);}/*** @After* 目標方法執行后執行,無論該方法是否拋出異常。能夠用于執行一些清理工作、記錄日志等* 無法直接獲取方法的返回值,沒有 JoinPoint* ************************************************************************************ */// Log 方法調用后輸出日志。 這里用 println 避免死循環
// @After("call(* android.util.Log.*(String, String))")
// public void afterAdvice() {
// System.out.println("Log method execution");
// }/*** @AfterReturning* 在目標方法成功返回后執行,可以獲取返回值。適合用于對成功結果的處理。* ************************************************************************************ */// 指定方法運行完成后獲取返回值
// @AfterReturning(pointcut = "execution(* com.aaaa.testplayer.MainActivity.getMsg())", returning = "result")
// public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
// Log.e("aaaaa","AfterReturning Method executed: " + joinPoint.getSignature().getName());
// Log.e("aaaaa","AfterReturning Returned value: " + result);
//
// // 獲取方法參數
// Object[] args = joinPoint.getArgs();
// System.out.println("MAfterReturning ethod arguments: ");
// for (Object arg : args) {
// Log.e("aaaaa","AfterReturning Argument: " + arg);
// }
// }/*** @AfterThrowing* 當目標方法拋出異常時執行。可以用來進行異常處理或日志記錄。* ************************************************************************************ */// 定義切點,匹配所有拋出異常的方法
// @AfterThrowing(pointcut = "execution(* com.aaaa.testplayer..*(..))", throwing = "ex")
// public void logAfterThrowing(Exception ex) {
// Log.e("aaaaa", "發現exception !!! " + ex.getMessage());
// // 這里可以添加其他處理邏輯,比如發送通知、記錄到數據庫等
// }
}
反編譯后可以看到對應的位置前新增了代碼?
多個切點的處理:
可以預先定義 Pointcut ,也可以對定義的 Pointcut 進行復用。
關于第三方庫?
aspectjx 可以對引入的三方庫也生效,但是對已經編譯好的 Android 系統類(例如 TextView)無法進行動態修改。這里使用三方庫?autosize 來示例。
因為?autosize 在部分設備上顯示有問題,需要調試。autosize 庫默認對日志進行 debug 級別輸出,但是在某些設備上,系統只輸出 info 級別及以上的 log,導致?autosize 日志全無,這里借助工具將?me.jessyan.autosize.utils.AutoSizeLog 中的日志級別強制提到 error 級。
首先需要引入三方庫:
然后在 aspectx 中記得加入對應的包名,不然織入不會生效。
編寫替換代碼,這里將全局的 Log.d 都替換為 Log.e 了。這個代碼上面有示例
運行后即可看到日志都被提升到 error
反編譯apk查看 ,可以看到 debug 級別輸出的代碼已經被替換。 warn 和 err 不受影響
關于高 gradle 版本?
根據github 官方文檔,gradle 支持只到 4.4 。那么高 gradle 需要額外處理:
強制使用 gradle 7.1.3
最小的改動就是將 gradl 版本指定為 7.1.3 .之前我用的 7.2.2 不行會報
Failed to apply plugin ‘android-aspectjx’. No such property: FD_INTERMEDIATES for class: com.android.builder.model.AndroidProject
gradle7.2及以上可使用wurensen重構過的版本來實現AOP操作。
參考:GitHub - LZ9/AspectjxDemo: 簡單的Aspectjx接入demo,兼容gradle7以上版本?
classpath 'io.github.wurensen:gradle-android-plugin-aspectjx:3.3.2'?
?
apply plugin: 'io.github.wurensen.android-aspectjx'?
aspectjx {enabled trueinclude 'com.aaaa.testplayer'exclude 'androidx', 'com.google', 'com.squareup', 'com.alipay', 'com.taobao', 'org.apache','kotlinx', 'org.jetbrains', "module-info", 'versions.9'
}