【Android】一文详解Android里的AOP编程
一文详解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
逻辑 - 不调用:事件被“吃掉”,原始逻辑不会执行
- 调用了:进入原始