一文詳解Android里的AOP編程
1. 基于 AspectJ(編譯期/打包期織入)
-
思路:用 AspectJ 編譯器在 編譯階段 或 Gradle Transform 階段,把切面邏輯織入 class / bytecode。
-
特點:
- 能實現類似 Spring AOP 的注解切面,支持
@Around
、@Before
、@After
等。 - 典型應用:埋點、性能監控、日志采集。
- 能實現類似 Spring AOP 的注解切面,支持
-
集成方式:
-
使用 Hujiang/gradle_plugin_android_aspectjx(支持 Gradle 插件織入)。
-
在切面類里寫:
@Aspect public class LogAspect {@Pointcut("execution(* com.example.myapp..*(..))")public void methodPointcut() {}@Before("methodPointcut()")public void beforeMethod(JoinPoint joinPoint) {Log.d("AOP", "調用方法: " + joinPoint.getSignature());} }
-
2. 基于 ASM / Javassist(字節碼修改)
- 思路:在 編譯后 / 打包前 修改字節碼,插入所需邏輯。
- 特點:
- 更底層、更靈活,但開發成本高。
- 一般用于統一插樁:如所有
setImageBitmap()
加水印。
- 實現方式:
- 自定義
Transform
,用 ASM 或 Javassist 遍歷 class 文件并修改。 - 常見框架:ByteX、booster、ASM 手寫工具。
- 自定義
3. 基于 動態代理 / 反射(運行時 AOP)
-
思路:利用 Java 動態代理 或 CGLIB(在 Android 上不常用) 在運行時生成代理對象。
-
限制:
- 只能代理 接口(因為 JDK 動態代理只能代理接口方法)。
- 對 Android 性能有一定影響(尤其是頻繁調用)。
-
適用場景:
- JSBridge、接口統一攔截、埋點 SDK。
-
示例:
public class ProxyHandler implements InvocationHandler {private final Object target;public ProxyHandler(Object target) {this.target = target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Log.d("AOP", "調用前: " + method.getName());Object result = method.invoke(target, args);Log.d("AOP", "調用后: " + method.getName());return result;} }// 使用 MyInterface proxy = (MyInterface) Proxy.newProxyInstance(MyInterface.class.getClassLoader(),new Class[]{MyInterface.class},new ProxyHandler(new MyInterfaceImpl()) );
4. 基于 Hook / 插樁框架
- 思路:通過系統 Hook 框架或 Xposed,對方法進行攔截。
- 適用場景:逆向分析、黑科技應用,或者企業內的監控 SDK。
- 常用框架:
- Xposed / EdXposed
- Epic(美團的運行時 Hook 框架,支持 Android ART)
- SandHook 等。
5. 對比與建議
方案 | 時機 | 優點 | 缺點 | 適合場景 |
---|---|---|---|---|
AspectJ | 編譯/打包期 | 寫法優雅,貼近 Spring AOP | 編譯速度慢,Gradle 配置復雜 | 日志埋點、性能監控 |
ASM/Javassist | 編譯/打包期 | 靈活,性能開銷低 | 學習成本高 | 全局插樁、修改框架方法 |
動態代理 | 運行時 | 實現快,適合接口 | 只能代理接口,性能差 | SDK 接口攔截、調試工具 |
Xposed/Epic | 運行時 | 功能強大 | 需 root/侵入性強 | 第三方 Hook、逆向 |
寫一個 Android 上 AspectJ AOP Demo,實現 攔截所有 View.OnClickListener 的點擊事件,并做點擊埋點(比如輸出日志)。
這個 Demo 分三部分:
- Gradle 配置 AspectJ 插件
- 定義切面類 @Aspect
- 測試按鈕點擊是否被攔截
6. Gradle 配置
首先在 app/build.gradle
里加上 AspectJX 插件(常用的開源實現是 Hujiang AspectJX):
plugins {id 'com.android.application'id 'android-aspectjx' // 加上這一行
}android {namespace "com.example.aopdemo"compileSdk 34defaultConfig {applicationId "com.example.aopdemo"minSdk 24targetSdk 34versionCode 1versionName "1.0"}
}dependencies {implementation 'org.aspectj:aspectjrt:1.9.7'
}
根目錄 build.gradle 里要加上插件 classpath:
buildscript {dependencies {classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'}
}
7. 定義切面類
在 app/src/main/java/com/example/aopdemo/aspect/ClickAspect.java
里寫一個切面類:
package com.example.aopdemo.aspect;import android.util.Log;
import android.view.View;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;@Aspect
public class ClickAspect {private static final String TAG = "AOP_CLICK";/*** 定義切點:攔截所有 View.OnClickListener 的 onClick(View) 方法*/@Pointcut("execution(* android.view.View.OnClickListener.onClick(..))")public void onClickMethod() {}/*** 環繞通知:在點擊前后都能插入邏輯*/@Around("onClickMethod()")public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {// 獲取參數(點擊的 View)Object[] args = joinPoint.getArgs();if (args != null && args.length > 0 && args[0] instanceof View) {View view = (View) args[0];int id = view.getId();String viewName = view.getResources().getResourceEntryName(id);Log.d(TAG, "點擊事件埋點: viewId=" + id + " viewName=" + viewName);}// 執行原始方法(必須,不然點擊邏輯會被攔截掉)joinPoint.proceed();}
}
8. Activity 測試
在 MainActivity.java
里隨便放個按鈕:
package com.example.aopdemo;import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;import androidx.appcompat.app.AppCompatActivity;public class MainActivity extends AppCompatActivity {private static final String TAG = "MainActivity";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);Button btn = findViewById(R.id.btn_test);btn.setOnClickListener(v -> Log.d(TAG, "按鈕邏輯被執行"));}
}
activity_main.xml
:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:gravity="center"android:layout_width="match_parent"android:layout_height="match_parent"><Buttonandroid:id="@+id/btn_test"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="點我試試"/>
</LinearLayout>
9. 運行效果
點擊按鈕時,Logcat 會輸出兩條日志:
D/AOP_CLICK: 點擊事件埋點: viewId=2131230890 viewName=btn_test
D/MainActivity: 按鈕邏輯被執行
? 這樣就實現了 全局點擊埋點 AOP。
后續可以把 Log.d
換成上報到埋點 SDK、神策、Firebase 等。
10. 原理解析
在 AspectJ 里,ProceedingJoinPoint
是一個 連接點(JoinPoint)的運行時對象,它代表了當前被攔截的方法調用。
10. 1 它的來源
ProceedingJoinPoint
繼承自JoinPoint
,專門用于@Around
環繞通知。JoinPoint
本身只能“看”,不能“改”;而ProceedingJoinPoint
可以“執行原方法”,即調用proceed()
。
10.2 它能拿到什么
在 @Around
方法里可以通過它獲取很多信息:
方法 | 說明 | 示例 |
---|---|---|
joinPoint.getArgs() | 獲取目標方法的參數數組 | [View v] |
joinPoint.getTarget() | 獲取被代理對象(目標對象) | 某個 OnClickListener 實例 |
joinPoint.getThis() | 獲取代理對象(AOP 生成的代理類) | 代理后的對象 |
joinPoint.getSignature() | 獲取方法簽名 | void onClick(View) |
joinPoint.getKind() | 獲取連接點類型 | method-execution |
joinPoint.getSourceLocation() | 獲取源碼位置(類名、行號) | MainActivity$1.onClick(MainActivity.java:27) |
10.3 最重要的 proceed()
joinPoint.proceed()
:執行原始方法(帶上原始參數)。joinPoint.proceed(Object[] args)
:可以 修改參數后再執行。
比如我們在點擊埋點的時候,也可以偷偷改掉參數:
@Around("onClickMethod()")
public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {Object[] args = joinPoint.getArgs();if (args != null && args.length > 0 && args[0] instanceof View) {View view = (View) args[0];Log.d("AOP_CLICK", "點擊: " + view.getId());}// 執行原方法(必須,不然點擊邏輯不會繼續)joinPoint.proceed(args);
}
10.4 在點擊埋點場景里
joinPoint.getTarget()
👉 實際上就是View.OnClickListener
對象joinPoint.getArgs()[0]
👉 傳入的View v
joinPoint.proceed()
👉 真正調用listener.onClick(v)
📌 總結一句:
ProceedingJoinPoint
= 運行時對當前方法調用的描述 + 能控制是否繼續執行原方法。
畫一個調用流程圖,展示 點擊按鈕 → AOP 切面 → proceed() → 原始 onClick 方法 的執行路徑。
🔍 解釋:
- 用戶點按鈕 → 系統調用
OnClickListener.onClick()
- 因為我們在方法上織入了 AOP → 先進入切面(
@Around
) - 切面邏輯(埋點、打印日志、防抖動判斷…)
joinPoint.proceed()
決定是否繼續調用原始方法:- 調用了:進入原始
onClick
邏輯 - 不調用:事件被“吃掉”,原始邏輯不會執行
- 調用了:進入原始