在 Android 开发中,AspectJ 是一个强大的工具,用于实现 AOP(面向切面编程)。通过 AspectJ,开发者可以在不修改现有代码的情况下,在编译时或运行时为应用程序添加横切关注点,比如日志记录、性能监控、权限验证等。AspectJ 在 Android 项目中的使用涉及一些特定配置,下面是详细的步骤和示例。
1. 环境设置
1.1 添加 AspectJ 依赖
首先,在你的 Android 项目中,需要添加 AspectJ 的依赖项。通常使用 AspectJ 的 Gradle 插件 来集成 AspectJ。
在项目的 build.gradle 中添加 AspectJ 插件和依赖:
gradle
复制代码
buildscript {
repositories {
google()
mavenCentral()
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
classpath 'com.android.tools.build:gradle:7.0.4'
classpath 'io.github.wurensen:gradle-android-plugin-aspectjx:3.3.2'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
然后,在应用模块的 build.gradle 中应用插件:
gradle
复制代码
apply plugin: 'com.android.application'
apply plugin: 'io.github.wurensen.android-aspectjx'
android {
compileSdkVersion 33
defaultConfig {
applicationId "com.example.myapp"
minSdkVersion 21
targetSdkVersion 33
versionCode 1
versionName "1.0"
}
}
dependencies {
implementation 'org.aspectj:aspectjrt:1.9.7'
}
1.2 配置 AspectJ
确保你正确配置了 AspectJ 插件,它将负责在编译过程中将切面代码织入到应用中。
2. 创建切面类
在 Android 项目中,使用 AspectJ 创建切面类来定义横切关注点的处理逻辑。
java
复制代码
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
import android.util.Log;
@Aspect
public class LoggingAspect {
@Before("execution(* com.example.myapp.MainActivity.on*(..))")
public void beforeMethod() {
Log.d("AspectJ", "Method execution started");
}
@After("execution(* com.example.myapp.MainActivity.on*(..))")
public void afterMethod() {
Log.d("AspectJ", "Method execution finished");
}
@Around("execution(* com.example.myapp.MainActivity.on*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
Log.d("AspectJ", "Before method execution");
Object result = joinPoint.proceed();
Log.d("AspectJ", "After method execution");
return result;
}
}
在这个示例中:
@Aspect:标记类为切面。@Before:定义前置通知,在方法执行前运行。@After:定义后置通知,在方法执行后运行。@Around:定义环绕通知,可以在方法执行前后运行并控制方法的执行。
这里的切点表达式 "execution(* com.example.myapp.MainActivity.on*(..))" 表示拦截 MainActivity 中所有以 on 开头的方法,例如 onCreate、onStart 等。
3. 编译和运行
配置好 AspectJ 后,编译你的 Android 项目。AspectJ 会在编译期间将你的切面逻辑织入到目标方法中。编译完成后,你可以运行应用并在日志中查看 AspectJ 的切面日志输出。
4. 注意事项
- 性能影响:AspectJ 会在编译期间或类加载时进行代码织入,这可能会增加编译时间。虽然运行时性能通常不会受到显著影响,但要注意复杂切点可能会导致额外的性能开销。
- 混淆配置:如果你的应用使用了 ProGuard 或 R8 进行代码混淆,确保配置文件中保留 AspectJ 的类和切面。
proguard
复制代码
-keep class org.aspectj.** { *; }
-keep class * extends org.aspectj.lang.annotation.Aspect { *; }
- 兼容性:AspectJ 在 Android 上的支持相对成熟,但某些特殊情况下可能会出现兼容性问题,特别是在使用 Android 特定的构建工具和 Gradle 插件时。
5. AspectJ 与 Android 的常见用例
- 日志记录:自动在方法执行前后记录日志,便于调试和监控。
- 性能监控:记录方法执行时间,帮助识别性能瓶颈。
- 权限检查:在执行敏感操作前自动检查权限,简化权限管理逻辑。
- 异常处理:统一捕获和处理异常,避免代码中散布大量的 try-catch 块。
5.1 方法执行耗时(用于app 启动性能检测)
/**
* desc : 布局加载耗时注解
*/
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
annotation class MethodTime
@Aspect
class MethodTimeAspect {
/**
* 方法切入点
*/
@Pointcut("execution(@com.hjq.base.aop.MethodTime * *(..))")
fun method() {
}
/**
* 在连接点进行方法替换
*/
@Around("method() && @annotation(methodTime)")
fun aroundJoinPoint(joinPoint: ProceedingJoinPoint, methodTime: MethodTime?) {
val signature = joinPoint.signature
//开始处打点
val name = signature.toShortString()
val startTime = System.currentTimeMillis()
try {
joinPoint.proceed()
} catch (throwable: Throwable) {
throwable.printStackTrace()
}
//结束打点,并计算耗时。
val costTime = System.currentTimeMillis() - startTime
LogUtils.d("方法耗时:", "method- " + name + " 耗时:" + costTime + "ms")
}
}
5.2 单击事件
/**
*
* desc : 防重复点击注解
*/
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
annotation class SingleClick(
/**
* 快速点击的间隔
*/
val value: Long = 1000
)
package com.hjq.base.aop
import com.blankj.utilcode.util.LogUtils
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Pointcut
import org.aspectj.lang.reflect.CodeSignature
/**
* desc : 防重复点击处理
*/
@Aspect
class SingleClickAspect {
/**
* 最近一次点击的时间
*/
private var mLastTime: Long = 0
/** 最近一次点击的标记 */
private var mLastTag: String? = null
/**
* 方法切入点
*/
@Pointcut("execution(@com.hjq.base.aop.SingleClick * *(..))") //com.hjq.base.aop.SingleClick
fun method() {
}
/**
* 在连接点进行方法替换
*/
@Around("method() && @annotation(singleClick)")
@Throws(Throwable::class)
fun aroundJoinPoint(joinPoint: ProceedingJoinPoint, singleClick: SingleClick) {
val codeSignature = joinPoint.signature as CodeSignature
// 方法所在类
val className = codeSignature.declaringType.name
// 方法名
val methodName = codeSignature.name
// 构建方法 TAG
val builder = StringBuilder("$className.$methodName")
builder.append("(")
val parameterValues = joinPoint.args
for (i in parameterValues.indices) {
val arg = parameterValues[i]
if (i == 0) {
builder.append(arg)
} else {
builder.append(", ")
.append(arg)
}
}
builder.append(")")
val tag = builder.toString()
val currentTimeMillis = System.currentTimeMillis()
if (currentTimeMillis - mLastTime < singleClick.value && tag == mLastTag) {
LogUtils.i("SingleClick", (currentTimeMillis - mLastTime).toString() + " 毫秒内发生快速点击")
return
}
mLastTime = currentTimeMillis
mLastTag = tag
// 执行原方法
joinPoint.proceed()
}
}
5.3 权限申请
/**
*
* desc : 权限申请注解
*/
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
annotation class Permissions(
/**
* 需要申请权限的集合
*/
val value: Array<String>
)
package com.hjq.common.aop
import android.app.Activity
import com.blankj.utilcode.util.LogUtils
import com.hjq.base.manager.ActivityManager
import com.hjq.common.PermissionCallback
import com.hjq.permissions.XXPermissions
import org.aspectj.lang.ProceedingJoinPoint
import org.aspectj.lang.annotation.Around
import org.aspectj.lang.annotation.Aspect
import org.aspectj.lang.annotation.Pointcut
/**
* desc : 权限申请处理
*/
@Suppress("unused")
@Aspect
class PermissionsAspect {
/**
* 方法切入点
*/
@Pointcut("execution(@com.hjq.common.aop.Permissions * *(..))")
fun method() {}
/**
* 在连接点进行方法替换
*/
@Around("method() && @annotation(permissions)")
fun aroundJoinPoint(joinPoint: ProceedingJoinPoint, permissions: Permissions) {
var activity: Activity? = null
// 方法参数值集合
val parameterValues: Array<Any?> = joinPoint.args
for (arg: Any? in parameterValues) {
if (arg !is Activity) {
continue
}
activity = arg
break
}
if ((activity == null) || activity.isFinishing || activity.isDestroyed) {
activity = ActivityManager.getInstance().getTopActivity()
}
if ((activity == null) || activity.isFinishing || activity.isDestroyed) {
LogUtils.e("The activity has been destroyed and permission requests cannot be made")
return
}
requestPermissions(joinPoint, activity, permissions.value)
}
private fun requestPermissions(joinPoint: ProceedingJoinPoint, activity: Activity, permissions: Array<out String>) {
XXPermissions.with(activity)
.permission(*permissions)
.request(object : PermissionCallback() {
override fun onGranted(permissions: MutableList<String>, allGranted: Boolean) {
if (allGranted) {
try {
// 获得权限,执行原方法
joinPoint.proceed()
} catch (e: Throwable) {
// CrashReport.postCatchedException(e)
}
} }
})
}
}
7. Gradle 8.0不再支持
在 Gradle 8.0 中,android.registerTransform API 已被废弃,并且无法再使用。这对于依赖于 Transform API 的插件(例如 AspectJX 或其他进行字节码操作的插件)会造成很大影响。这个变化要求开发者转向新的替代 API 来实现类似的功能。
7.1. 问题背景
Transform API 允许插件在构建过程中对编译后的 .class 文件或 .dex 文件进行修改。在 Android 项目中,这通常用于字节码插桩、AOP(如 AspectJX)等操作。然而,随着 Android Gradle Plugin (AGP) 的更新,registerTransform 已经被标记为过时,最终在 Gradle 8.0 中完全移除。
7.2. 替代方案
Android Gradle Plugin 团队建议开发者使用新的 Gradle API 来替代 Transform API。以下是一些替代方案:
7.2.1 使用 AndroidComponentsExtension
AGP 4.1+ 提供了 AndroidComponentsExtension,它允许你在构建过程中获取编译后的 .class 文件,并进行处理。这是替代 Transform API 的主要推荐方式。
kotlin
复制代码
android {
// 获取 AndroidComponentsExtension 并注册一个新的 Java 任务
onVariants { variant ->
variant.transformClassesWith(
MyClassVisitorFactory::class.java,
InstrumentationScope.PROJECT
) {
// 传递参数或配置
}
}
}
在这个例子中,MyClassVisitorFactory 是一个自定义的字节码处理工厂,用于创建 ClassVisitor 实例来处理字节码。
7.2.2 自定义编译任务
如果需要更复杂的字节码处理逻辑,可以考虑创建自定义的 Gradle 任务。这些任务可以在构建过程中执行,并处理字节码文件。
kotlin
复制代码
tasks.register("processClasses") {
doLast {
// 处理 .class 文件的逻辑
}
}
tasks.named("compileJava") {
finalizedBy("processClasses")
}
这个方案更加灵活,但需要更多手动配置。
7.2.3 使用 AGP 提供的字节码工具
Android Gradle Plugin 提供了一些内置工具用于字节码操作和优化,如 R8。虽然它们可能不完全等同于 Transform API,但对于特定场景(如代码混淆、压缩)来说,它们是推荐的替代方案。
7.3. 迁移策略
如果你的项目或插件依赖于 Transform API,你可能需要执行以下步骤来迁移到新的 API:
- 评估现有代码:识别你使用
TransformAPI 的位置和方式,了解其在构建过程中处理的内容。 - 选择替代方案:根据你的需求,决定使用
AndroidComponentsExtension、自定义任务,还是其他工具。 - 更新插件或脚本:重写或修改你的 Gradle 插件或脚本,使其与新的 API 兼容。
- 测试:彻底测试你的构建过程,确保迁移后的构建产物(APK、AAB 等)在功能和性能上与旧版本一致。
7.4. 总结
随着 Gradle 和 Android Gradle Plugin 的演进,某些旧 API(如 android.registerTransform)被废弃和移除,要求开发者转向更现代化、更优化的替代方案。使用 AndroidComponentsExtension 或自定义编译任务,可以有效替代旧的 Transform API,虽然这可能需要一定的学习和迁移成本,但有助于保持项目的长期可维护性和兼容性。