当前位置: 首页 > news >正文

【Android】一文详解Android里的AOP编程

一文详解Android里的AOP编程

1. 基于 AspectJ(编译期/打包期织入)

  • 思路:用 AspectJ 编译器在 编译阶段Gradle Transform 阶段,把切面逻辑织入 class / bytecode。

  • 特点

    • 能实现类似 Spring AOP 的注解切面,支持 @Around@Before@After 等。
    • 典型应用:埋点、性能监控、日志采集。
  • 集成方式

    • 使用 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,用 ASMJavassist 遍历 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 分三部分:

  1. Gradle 配置 AspectJ 插件
  2. 定义切面类 @Aspect
  3. 测试按钮点击是否被拦截

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 方法 的执行路径。


用户点击按钮
系统触发 View.OnClickListener.onClick()
进入 AOP 切面 @Around 通知
执行切面逻辑(埋点/日志/统计)
调用 joinPoint.proceed()?
拦截掉,不执行原始逻辑
执行原始 onClick 方法
按钮原有逻辑被执行
流程结束

🔍 解释

  1. 用户点按钮 → 系统调用 OnClickListener.onClick()
  2. 因为我们在方法上织入了 AOP → 先进入切面(@Around
  3. 切面逻辑(埋点、打印日志、防抖动判断…)
  4. joinPoint.proceed() 决定是否继续调用原始方法:
    • 调用了:进入原始 onClick 逻辑
    • 不调用:事件被“吃掉”,原始逻辑不会执行
http://www.dtcms.com/a/340554.html

相关文章:

  • 专题:2025全球消费趋势与中国市场洞察报告|附300+份报告PDF、原数据表汇总下载
  • 【0基础PS】图片格式
  • LWIP的TCP协议
  • Chrome 中的 GPU 加速合成
  • Google Chrome v139.0.7258.139 便携增强版
  • IP查找的方法、工具及应用场景
  • 让Chrome信任自签名证书
  • Google Chrome 扩展不受信任 - 不受支持的清单版本 解决方案
  • 单北斗GNSS位移监测技术解析
  • 爬虫逆向--Day16Day17--核心逆向案例3(拦截器关键字、路径关键字、请求堆栈、连续请求)
  • 欧州服务器String 转 double 有BUG?
  • Ubuntu 上安装 MongoDB
  • 【数据库】Oracle学习笔记整理之六:ORACLE体系结构 - 重做日志文件与归档日志文件(Redo Log Files Archive Logs)
  • RabbitMQ:生产者可靠性(生产者重连、生产者确认)
  • 多模型创意视频生成平台
  • 超高清与低延迟并行:H.266 在行业视频中的落地图谱
  • 【嵌入式电机控制#34】FOC:意法电控驱动层源码解析——HALL传感器中断(不在两大中断内,但重要)
  • 关联查询(left/right)优化
  • 50GHz+示波器:精准捕捉超高频信号
  • 激光雷达点云平面拟合与泊松重建对比分析
  • 【ElasticSearch】ElasticSearch Overview
  • Day 40:训练和测试的规范写法
  • 【深度学习新浪潮】空天地数据融合技术在城市三维重建中的应用
  • 学习嵌入式的第二十二天——数据结构——双向链表
  • 前端图片压缩实战:体积直降 80%,LCP 提升 2 倍
  • 数字化图书管理系统设计实践(java)
  • 【考研408数据结构-04】 栈与队列:受限的线性表
  • Java FTPClient详解:高效文件传输指南
  • 用好 Elasticsearch Ruby 传输层elastic-transport
  • Redisson3.14.1及之后连接阿里云redis代理模式,使用分布式锁:ERR unknown command ‘WAIT‘