目录
  1. 1. 一、AspectJ AOP 理论基础
    1. 1.1. 1.1 AOP 核心概念
    2. 1.2. 1.2 为什么 Android 全埋点使用 Post-compile Weaving?
  2. 2. 二、Gradle 集成:Android AspectJX 插件
    1. 2.1. 2.1 插件配置
    2. 2.2. 2.2 AspectJX 的工作原理
  3. 3. 三、完整的点击埋点 Aspect 实现
    1. 3.1. 3.1 核心 Aspect:onClick 方法拦截
    2. 3.2. 3.2 更多 Pointcut 示例
  4. 4. 四、Kotlin 兼容性的坑与解决方案
    1. 4.1. 4.1 Kotlin Lambda 的 onClick
    2. 4.2. 4.2 Suspend 函数的兼容性
    3. 4.3. 4.3 Inline / Reified 函数的排除
  5. 5. 五、构建性能分析与优化
    1. 5.1. 5.1 AspectJ 对构建时间的影响
    2. 5.2. 5.2 优化策略
    3. 5.3. 5.3 Composable 函数(Jetpack Compose)
  6. 6. 六、AspectJ 与其他方案的全面对比
  7. 7. 七、ProGuard/R8 规则
  8. 8. 面试常考问题
【全埋点方案系列】AppClick全埋点之AspectJ处理

AspectJ 是面向切面编程(AOP)在 Java 领域的标准实现。在全埋点场景中,AspectJ 可以在编译期将埋点代码织入指定的方法(如 View.OnClickListener.onClick()),实现零业务代码侵入的全自动埋点。

一、AspectJ AOP 理论基础

1.1 AOP 核心概念

┌─────────────────────────────────────────────────────┐
│ AspectJ 核心概念 │
├─────────────────────────────────────────────────────┤
│ │
│ Join Point (连接点) │
│ ├── method execution: 方法执行时 │
│ ├── method call: 方法调用时 │
│ ├── constructor execution: 构造器执行 │
│ ├── field get/set: 字段读写 │
│ ├── exception handler: 异常处理 │
│ ├── class initialization: 类初始化 │
│ └── advice execution: 通知执行 │
│ │
│ Pointcut (切入点) │
│ ├── execution(* View.OnClickListener.onClick(..)) │
│ ├── call(* *.setOnClickListener(..)) │
│ ├── within(com.example..*) │
│ ├── @annotation(TrackClick) │
│ └── 组合: &&, ||, ! │
│ │
│ Advice (通知) │
│ ├── @Before: 方法执行前 │
│ ├── @After: 方法返回后 (正常/异常都执行) │
│ ├── @AfterReturning: 方法正常返回后 │
│ ├── @AfterThrowing: 方法抛出异常后 │
│ └── @Around: 环绕 (可控制是否执行原方法) │
│ │
│ Weaving (织入) │
│ ├── Compile-time (编译时织入) │
│ ├── Post-compile (编译后织入) ← Android 使用此类型 │
│ └── Load-time (类加载时织入) │
│ │
└─────────────────────────────────────────────────────┘

1.2 为什么 Android 全埋点使用 Post-compile Weaving?

Android 的构建流程是先由 javac/kotlinc 编译为 .class,再由 d8/r8 转换为 .dex。AspectJ 在 .class 阶段(javac 之后、dx 之前)织入,属于 Post-compile Weaving(也叫 Binary Weaving)。这种方式不需要修改源码,不需要 AspectJ 编译器替代 javac,与 Gradle 构建系统无缝集成。

二、Gradle 集成:Android AspectJX 插件

2.1 插件配置

// 项目根 build.gradle
buildscript {
dependencies {
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'
}
}

// app/build.gradle
apply plugin: 'android-aspectjx'

aspectjx {
// 织入范围
include 'com.example.app' // 只处理自己的包
exclude 'com.google', 'androidx', 'com.squareup', 'com.google.android'

// 是否启用 (debug 可以关闭加快构建)
enabled true

// 忽略 AspectJ 编译警告
ajcArgs {
arg '-Xlint:ignore'
arg '-showWeaveInfo' // 查看织入了哪些方法
}
}

2.2 AspectJX 的工作原理

┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│ .kt/.java│ → │ kotlinc/ │ → │ AspectJX │ → │ d8/r8 │ → .dex
│ 源码 │ │ javac │ │ (ajc) │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘

│ Post-compile Weaving
│ 在 .class 字节码中织入 Advice

┌─────────────────┐
│ 扫描所有 .class │
│ 匹配 Pointcut │
│ 修改字节码 │
│ 写回 .class │
└─────────────────┘

AspectJX 插件在 Gradle Transform 阶段拦截所有 class 文件,调用 AspectJ 编译器(ajc)进行织入。这是一个独立的构建步骤,会增加构建时间。

三、完整的点击埋点 Aspect 实现

3.1 核心 Aspect:onClick 方法拦截

/**
* 点击埋点的 Aspect
* 拦截所有实现了 View.OnClickListener 的 onClick 方法
*/
@Aspect
class ClickTrackingAspect {

/**
* Pointcut 1: 拦截 onClick(View) 方法执行
*
* execution(...) 匹配方法执行
* * 表示任意返回值
* android.view.View.OnClickListener+.onClick 表示接口及所有子类的 onClick
* (android.view.View) 匹配参数类型
*
* 注意:Kotlin lambda onClick 编译后也实现 OnClickListener,会被自动匹配
*/
@Pointcut("execution(* android.view.View.OnClickListener+.onClick(android.view.View))")
fun onClickMethod() {
// 空方法,仅作为 Pointcut 的声明载体
}

/**
* Pointcut 2: 拦截 setOnClickListener 调用
* 可用于记录 Listener 的注册时机和注册栈
*/
@Pointcut("call(* android.view.View.setOnClickListener(..))")
fun setOnClickListener() {}

/**
* Pointcut 3: 排除系统类
*/
@Pointcut("!within(android..*) && !within(androidx..*)")
fun notSystemClass() {}

/**
* @Around 环绕通知:最完整的控制力
*
* 为什么选择 @Around 而非 @Before
* 1. 可以在 proceed() 前后分别记录时间,计算业务逻辑耗时
* 2. 可以 try-catch 捕获业务异常并上报
* 3. 可以通过条件判断是否执行原始逻辑(如 A/B 测试中不执行某功能)
*/
@Around("onClickMethod() && notSystemClass()")
@Throws(Throwable::class)
fun aroundClick(joinPoint: ProceedingJoinPoint): Any? {
// ===== Step 0: 提取上下文 =====
val view = joinPoint.args[0] as? View
?: return joinPoint.proceed() // 安全降级:无 View 时直接执行原始逻辑

// 获取 this 对象(OnClickListener 实现类的实例)
val target = joinPoint.`this`

// 获取方法签名
val signature = joinPoint.signature

// ===== Step 1: 前置处理 - 记录开始时间和环境 =====
val clickStartTime = SystemClock.elapsedRealtime()
val clickProperties = buildClickProperties(view, target, signature)

// ===== Step 2: 执行原始业务逻辑 =====
val result: Any?
try {
result = joinPoint.proceed() // 执行原始的 onClick(View) 方法

// ===== Step 3: 成功后的后置处理 =====
val duration = SystemClock.elapsedRealtime() - clickStartTime
trackClickSuccess(view, clickProperties, duration)

} catch (throwable: Throwable) {
// ===== Step 4: 异常处理 =====
val duration = SystemClock.elapsedRealtime() - clickStartTime
trackClickException(view, clickProperties, duration, throwable)
throw throwable // 重新抛出,不影响业务异常处理
}

return result
}

/**
* 构建点击属性
*/
private fun buildClickProperties(
view: View,
target: Any?,
signature: Signature
): Map<String, Any?> {
val properties = mutableMapOf<String, Any?>()

// View 信息
properties["view_class"] = view.javaClass.name
properties["view_id"] = view.id
try {
if (view.id != View.NO_ID) {
properties["view_id_name"] = view.resources.getResourceEntryName(view.id)
}
} catch (_: Resources.NotFoundException) {}

// Listener 信息
properties["listener_class"] = target?.javaClass?.name ?: "unknown"

// 方法信息
properties["method_name"] = signature.name
properties["declaring_type"] = signature.declaringType?.name

// 文本内容
(view as? TextView)?.let {
properties["view_text"] = it.text?.toString()?.take(100) ?: ""
}

// 页面上下文
properties["page_name"] = getCurrentActivityName(view)

// 时间戳
properties["click_timestamp"] = System.currentTimeMillis()

return properties
}

/**
* 上报点击成功事件
*/
private fun trackClickSuccess(
view: View,
properties: Map<String, Any?>,
durationMs: Long
) {
// 在子线程上报,不阻塞主线程
TrackerExecutor.execute {
AnalyticsSDK.track("app_click", properties.toMutableMap().apply {
put("status", "success")
put("duration_ms", durationMs)
})
}

// 慢点击告警
if (durationMs > 200) {
TrackerExecutor.execute {
AnalyticsSDK.track("slow_click", mapOf(
"view_class" to view.javaClass.name,
"duration_ms" to durationMs,
"view_id" to view.id
))
}
}
}

/**
* 上报点击异常事件
*/
private fun trackClickException(
view: View,
properties: Map<String, Any?>,
durationMs: Long,
throwable: Throwable
) {
TrackerExecutor.execute {
AnalyticsSDK.track("click_exception", properties.toMutableMap().apply {
put("status", "exception")
put("duration_ms", durationMs)
put("exception_type", throwable.javaClass.name)
put("exception_message", throwable.message ?: "")
put("exception_stack", throwable.stackTrace.take(5).joinToString("\n"))
})
}
}

/**
* 获取当前 Activity 名称
*/
private fun getCurrentActivityName(view: View): String {
return (view.context as? Activity)?.javaClass?.simpleName
?: view.context.javaClass.simpleName
}
}

3.2 更多 Pointcut 示例

@Aspect
class RichTrackingAspect {

// ========== Activity 生命周期埋点 ==========

@Pointcut("execution(* android.app.Activity+.onCreate(android.os.Bundle))")
fun activityOnCreate() {}

@Around("activityOnCreate()")
fun aroundActivityCreate(joinPoint: ProceedingJoinPoint) {
val activity = joinPoint.`this` as Activity
AnalyticsSDK.track("page_create", mapOf(
"page_name" to activity.javaClass.simpleName,
"page_class" to activity.javaClass.name,
"timestamp" to System.currentTimeMillis()
))
joinPoint.proceed()
}

@Pointcut("execution(* android.app.Activity+.onResume())")
fun activityOnResume() {}

@After("activityOnResume()")
fun afterActivityResume(joinPoint: JoinPoint) {
val activity = joinPoint.`this` as Activity
AnalyticsSDK.track("page_view", mapOf(
"page_name" to activity.javaClass.simpleName
))
}

// ========== Dialog 点击埋点 ==========

@Pointcut("execution(* android.content.DialogInterface.OnClickListener+.onClick(..))")
fun dialogOnClick() {}

@Around("dialogOnClick()")
fun aroundDialogClick(joinPoint: ProceedingJoinPoint): Any? {
val dialog = joinPoint.args[0] as? DialogInterface
val which = joinPoint.args.getOrNull(1) as? Int ?: -1

AnalyticsSDK.track("dialog_click", mapOf(
"dialog_class" to dialog?.javaClass?.name,
"which_button" to which,
"button_name" to when (which) {
DialogInterface.BUTTON_POSITIVE -> "positive"
DialogInterface.BUTTON_NEGATIVE -> "negative"
DialogInterface.BUTTON_NEUTRAL -> "neutral"
else -> "item_$which"
}
))
return joinPoint.proceed()
}

// ========== RecyclerView / AdapterView 点击埋点 ==========

@Pointcut("execution(* android.widget.AdapterView.OnItemClickListener+.onItemClick(..))")
fun itemClick() {}

@Around("itemClick()")
fun aroundItemClick(joinPoint: ProceedingJoinPoint): Any? {
val parent = joinPoint.args[0] as? AdapterView<*> // ListView / GridView 等
val view = joinPoint.args[1] as? View
val position = joinPoint.args[2] as? Int ?: -1
val id = joinPoint.args[3] as? Long ?: -1L

if (view != null) {
AnalyticsSDK.track("list_item_click", mapOf(
"adapter_view" to parent?.javaClass?.simpleName,
"position" to position,
"item_id" to id,
"view_class" to view.javaClass.simpleName,
"view_text" to ((view as? TextView)?.text?.toString()?.take(100) ?: "")
))
}
return joinPoint.proceed()
}

// ========== MenuItem 点击埋点 ==========

@Pointcut("execution(* android.view.MenuItem.OnMenuItemClickListener+.onMenuItemClick(..))")
fun menuItemClick() {}

@Before("menuItemClick()")
fun beforeMenuItemClick(joinPoint: JoinPoint) {
val menuItem = joinPoint.args[0] as? MenuItem
AnalyticsSDK.track("menu_click", mapOf(
"menu_title" to (menuItem?.title?.toString() ?: ""),
"menu_id" to (menuItem?.itemId ?: -1)
))
}

// ========== 自定义注解:业务方显式标记 ==========

@Pointcut("execution(@com.example.annotation.TrackClick * *(..))")
fun annotatedWithTrackClick() {}

@Around("annotatedWithTrackClick()")
fun aroundAnnotatedMethod(joinPoint: ProceedingJoinPoint): Any? {
val method = (joinPoint.signature as MethodSignature).method
val annotation = method.getAnnotation(TrackClick::class.java)
val eventId = annotation.eventId

val startTime = System.currentTimeMillis()
val result = joinPoint.proceed()
val duration = System.currentTimeMillis() - startTime

AnalyticsSDK.track(eventId, mapOf(
"method" to method.name,
"duration_ms" to duration,
"timestamp" to startTime
))

return result
}
}

四、Kotlin 兼容性的坑与解决方案

4.1 Kotlin Lambda 的 onClick

Kotlin 代码 view.setOnClickListener { doSomething() } 编译后生成一个实现 View.OnClickListener 的匿名内部类,类名类似 MainActivity$onCreate$1。AspectJ 的 Pointcut execution(* View.OnClickListener+.onClick(..)) 会匹配到它的 onClick 方法。

反编译验证

// Kotlin 编译后的 Java 等价代码
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doSomething();
}
});
// AspectJ 织入后的字节码等价:
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TrackingHelper.trackClick(v); // ← AspectJ 插入
doSomething();
}
});

4.2 Suspend 函数的兼容性

AspectJ 对 Kotlin 的 suspend 函数支持有限。suspend fun onClick(view: View) 编译后会生成带有 Continuation 参数的方法,Pointcut 需要额外匹配 Continuation 参数签名。

// 处理 suspend 函数
@Pointcut("execution(* *(..)) && args(android.view.View, kotlin.coroutines.Continuation)")
fun suspendOnClick() {}

// 或使用更宽泛的匹配
@Pointcut("execution(* *.*(..)) && args(android.view.View, ..)")
fun anyMethodWithViewParam() {}

4.3 Inline / Reified 函数的排除

Kotlin 的 inline 函数在编译后被内联到调用点,其字节码本身不包含独立方法体。AspectJ 无法对 inline 函数的「执行」进行织入(因为该方法不存在运行时独立的调用),只能用 call(*) Pointcut 在调用点上织入。

// 排除 inline 函数
@Pointcut("!execution(* kotlin..*.*(..))")
fun notKotlinInline() {}

五、构建性能分析与优化

5.1 AspectJ 对构建时间的影响

测量方法:在 Gradle 中添加构建时间分析。

构建阶段              | 无 AspectJ | 有 AspectJ | 增加
--------------------|-----------|-----------|------
javac/kotlinc | 45s | 45s | 0s
AspectJ Weaving | 0s | 15-30s | +15-30s
dex (d8) | 20s | 20s | 0s
Total | 65s | 80-95s | +20-40%

5.2 优化策略

aspectjx {
// 1. 严格限定织入范围(最关键)
include 'com.example.app.click', 'com.example.app.event'
exclude 'com.google', 'androidx', 'com.squareup', 'com.facebook'

// 2. Debug 构建时关闭织入
enabled !project.hasProperty('debug')

// 3. 使用 AJC 的增量编译(需要 Ajc 1.9+)
ajcArgs {
arg '-incremental'
}
}

5.3 Composable 函数(Jetpack Compose)

AspectJ 无法直接织入 @Composable 函数。Compose 的编译器插件会在编译期重组函数,导致 AspectJ 在字节码层面看到的结构与源码完全不同。Compose 全埋点需要专门的方案(如 Compose 的 Modifier.clickable 拦截)。

六、AspectJ 与其他方案的全面对比

维度 AspectJ ASM Javassist Window.Callback OnClickListener 代理
实现方式 声明式 Pointcut/Advice 命令式字节码操作 源码字符串注入 运行时委托 运行时反射
学习曲线 中(AOP 概念) 高(JVM 指令) 低(Java 字符串) 低(委托模式) 低(反射)
灵活度 中(受 Pointcut 语法限制) 极高
编译速度 慢(+15-30s) 中等(+10-20s) 中等(+10-15s) 无影响 无影响
运行时开销 零(编译时织入) 零(编译时) 每次触摸 ~1ms 每次点击 ~0.1ms
Kotlin 支持 部分(coroutine 受限) 完全(通过 Java 字节码) 完全 完全 完全
Gradle 维护 需要插件(AspectJX) 需要 Transform(AGP 7-) 需要 Transform 无需 无需
AGP 8.0 插件待适配 需迁移至 AsmClassVisitorFactory 需迁移 完全兼容 完全兼容

七、ProGuard/R8 规则

# 保持 Aspect 类
-keep @org.aspectj.lang.annotation.Aspect class * { *; }

# 保持被 @Pointcut, @Around, @Before, @After 标记的方法
-keepclassmembers class * {
@org.aspectj.lang.annotation.Pointcut <methods>;
@org.aspectj.lang.annotation.Around <methods>;
@org.aspectj.lang.annotation.Before <methods>;
@org.aspectj.lang.annotation.After <methods>;
@org.aspectj.lang.annotation.AfterReturning <methods>;
@org.aspectj.lang.annotation.AfterThrowing <methods>;
}

# 保持 TrackingHelper(Aspect 中调用的辅助类)
-keep class com.example.tracking.ClickTrackingAspect { *; }
-keep class com.example.tracking.RichTrackingAspect { *; }
-keep class com.example.tracking.TrackerExecutor { *; }

# JoinPoint 相关类
-keep class org.aspectj.lang.JoinPoint { *; }
-keep class org.aspectj.lang.ProceedingJoinPoint { *; }
-keep class org.aspectj.lang.Signature { *; }

# 防止 R8 把 OnClickListener 匿名内部类优化掉
-keep class * implements android.view.View$OnClickListener {
void onClick(android.view.View);
}

面试常考问题

Q1:AspectJ 的织入时机是在 javac 编译之前还是之后?

AspectJ 在 Java 编译为 .class 文件之后、.class 打包为 .dex 之前执行。它修改的是字节码。在 Android Gradle 构建流程中,AspectJX 插件在 Transform 阶段插入,扫描所有 class 文件,对匹配 Pointcut 的方法进行字节码增强。具体流程是:源码 → javac/kotlinc → .class → ajc (AspectJ Compiler) 织入 → 织入后的 .class → d8/r8 → .dex。由于是在 javac 之后,AspectJ 看不到原始源码,只能看到编译后的字节码,因此对泛型、lambda 等 Java/Kotlin 语法糖的匹配依赖于 javac 生成的字节码结构。

Q2:@Around 与 @Before/@After 的选择?

全埋点场景推荐 @Around,原因:(1)可以在 joinPoint.proceed() 前后分别采集时间,计算点击处理的耗时;(2)能通过 try-catch 捕获原始 onClick 中的异常并上报,@Before 只能被动地在执行前插入代码,无法感知异常;(3)可以选择性地不执行原始 onClick 逻辑(如 A/B 测试中关闭某个功能的点击处理),@Before 无法阻止原始方法执行。缺点是 @Around 必须显式调用 proceed() 并处理返回值,代码略多。

Q3:AspectJ 如何处理 Kotlin 的 lambda onClick?

Kotlin 中 view.setOnClickListener { } 编译后生成实现 View.OnClickListener 的匿名内部类,其 onClick 方法符合 Pointcut execution(* View.OnClickListener+.onClick(View)),可以正常织入。反编译后可见 class 文件中实际生成了一个额外类(如 MainActivity$onCreate$1),AspectJ 对其字节码进行修改。但有一个特殊情况:如果 Kotlin 编译器对 lambda 使用了 -Xsam-conversions=class(sam 转换为匿名内部类而非 invokedynamic),字节码结构不同,AspectJ 可能匹配不上。不过默认的 Kotlin 编译行为是生成匿名内部类,所以大多数情况下正常工作。

Q4:AspectJX 插件在 AGP 8.0 中无法使用,如何迁移?

AGP 8.0 完全移除了旧的 Transform API。AspectJX 插件依赖 Transform API,因此无法直接在 AGP 8.0 上运行。迁移策略:(1)等待 AspectJX 官方适配 AGP 8.0 的新 Artifacts Transform API;(2)将 AspectJ 替换为 ASM 的 AsmClassVisitorFactory,这是 AGP 8.0 推荐的字节码操作方式;(3)如果必须继续使用 AspectJ,可以将织入步骤从 Gradle 插件中分离,在 CI 阶段独立运行 ajc 对 .class 文件进行后处理。无论哪种方案,都需要团队对构建流程有深入理解。

Q5:多个 Aspect 作用于同一个 Join Point 时的执行顺序是什么?

当多个 Aspect 的 Advice 匹配到同一个 Join Point 时,执行顺序取决于优先级。规则:(1)通过 @DeclarePrecedence 声明全局优先级,如 @DeclarePrecedence("SecurityAspect, TrackingAspect, LoggingAspect") 表示 Security 最先、Tracking 次之、Logging 最后;(2)同优先级的 Aspect,@Before 按字母序(类名)执行,@After 按字母序反向执行(洋葱模型);(3)在全埋点场景中,如果不显式指定优先级,多个埋点 Aspect 的执行顺序不确定,可能导致埋点信息不完整(如页面信息还未设置就触发了点击埋点)。建议显式声明优先级,将收集页面上下文的 Aspect 设为最高优先级(最先执行)。

AOSP 中相关源码:View.javaperformClick() 方法(frameworks/base/core/java/android/view/View.java)。AspectJ 自身是一个独立开源项目,其编译器 ajc 的源码位于 github.com/eclipse/org.aspectj

打赏
  • 微信
  • 支付宝

评论