AspectJ 在APM上的应用(三)
AspectJ 两种用法以及常见问题
前面两篇文章介绍了 AspectJ 的基础概念以及基于注解开发方式的语法,这篇文章总结了 AspectJ 的两种用法和 android-aspectjx 插件的常见问题。
AspectJ 的两种用法
我觉得以 Pointcut 切入点作为区分,AspectJ 有两种用法:(1)用自定义注解修饰切入点,精确控制切入点,属于侵入式;(2)不需要在切入点代码中做任何修改,属于非侵入式。
侵入式
侵入式用法,一般会使用自定义注解,以此作为选择切入点的规则。
下面以 JakeWharton 大神的 hugo 为例,分析自定义注解 AOP 的使用。hugo 是用于在开发环境中打印方法调用信息的,只会打印注解修饰的方法。
首先看下新增的自定义注解:
@Target({TYPE, METHOD, CONSTRUCTOR}) @Retention(CLASS)
public @interface DebugLog {
}
上面定义了@DebugLog
注解,可以修饰类、接口、方法和构造函数,可在 Class 文件中保留,编译期可用。更多关于 Java 注解的内容,请看之前的文章 探索注解之注解的基本概念。
再看 hugo 的切面代码,代码说明在注释中:
@Aspect
public class Hugo {
private static volatile boolean enabled = true;
@Pointcut("within(@hugo.weaving.DebugLog *)")
public void withinAnnotatedClass() {} // @DebugLog 修饰的类、接口的 Join Point
// synthetic 是内部类编译后添加的修饰语,所以 !synthetic 表示非内部类的
@Pointcut("execution(!synthetic * *(..)) && withinAnnotatedClass()")
public void methodInsideAnnotatedType() {} // 执行 @DebugLog 修饰的类、接口中的方法,不包括内部类中方法
@Pointcut("execution(!synthetic *.new(..)) && withinAnnotatedClass()")
public void constructorInsideAnnotatedType() {} // 执行 @DebugLog 修饰的类中的构造函数,不包括内部类的构造函数
@Pointcut("execution(@hugo.weaving.DebugLog * *(..)) || methodInsideAnnotatedType()")
public void method() {} // 执行 @DebugLog 修饰的方法,或者 @DebugLog 修饰的类、接口中的方法
@Pointcut("execution(@hugo.weaving.DebugLog *.new(..)) || constructorInsideAnnotatedType()")
public void constructor() {} // 执行 @DebugLog 修饰的构造函数,或者 @DebugLog 修饰的类中的构造函数
...
@Around("method() || constructor()")
public Object logAndExecute(ProceedingJoinPoint joinPoint) throws Throwable {
enterMethod(joinPoint); // 打印切入点方法名、参数列表
long startNanos = System.nanoTime();
Object result = joinPoint.proceed(); // 调用原来的方法
long stopNanos = System.nanoTime();
long lengthMillis = TimeUnit.NANOSECONDS.toMillis(stopNanos - startNanos);
exitMethod(joinPoint, result, lengthMillis); // 打印切入点方法名、返回值、方法执行时间
return result;
}
...
从上面代码可以看出 hugo 是以 @DebugLog 作为选择切入点的条件,只需要用 @DebugLog 注解类或者方法就可以打印方法调用的信息。
所以,可以看出侵入式 AspectJ 的特点:
- 需要自定义注解
- 切入点需要添加注解,会侵入切入点代码
- 不需要修改 Aspect 切面代码,就可以随意修改切入点
非侵入式
非侵入式,就是不需要使用额外的注解来修饰切入点,不用修改切入点的代码。
最近项目中在调研无埋点数据上报,不需要每次新增功能时都添加埋点代码,这种场景适合非侵入式的 AOP,我们采用的是 AspectJ 技术来实现。数据上报一般包括页面和事件统计,下面以 Activity、Fragment 页面统计以及点击事件统计为例,分析非侵入式 Aspectj 在 Android 项目中的使用。
Activity 页面统计
首先需要明白的一点是:AspectJ 无法在 Activity 中织入代码,因为 Activity 属于 android.jar,是安卓平台代码,Class 文件不会在编译时打包进 apk 中。但是项目中继承自 Activity 的子类可以作为切入点,因为编译期会变成 Class 文件。
网上一些统计 Activity 例子是这样:
@Before("execution(* android.app.Activity+.on**(..))")
public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
...
}
上面的代码是以 Activity 及其子类的 onXX 方法作为切入点,但是 Activity 类是无法切入的,而 Activity 的子类的话只能切入重写的 onXX 方法。
所以这种写法其实有两个问题:(1)如果 BaseActivity 和 其子类 XXActivity 都重写了 onPause 方法,那么两个方法都会织入 AOP 代码,所以 XXActivity 执行 onPause 方法时,会调用两次数据统计的 AOP 代码;(2)如果 XXActivity 没有重写 onDestroy 方法,那么就 XXActivity.onDestroy 方法就无法作为切入点。
我觉得有两种方式比较合理:
- 项目中有个 BaseActivity 作为所有 Activity 的基类,而且 BaseActivity 重写了 Activity 的所有 onXX 方法时,可以以 BaseActivity 作为切入点。Pointcut 表达式:
execution(* BaseActivity.on**(..))
- 使用 Android API 的 Application.ActivityLifecycleCallbacks,可以监听应用中所有 Activity 生命周期的变化,这也更为通用。
Fragment 页面统计
公司项目中使用的是 support-v4 包中的 Fragment,support-v4 会作为依赖一起编译打包进 apk,所以可以直接切入 Fragment 中。
需要统计的 Fragment 的显示与隐藏,不过不能仅仅依靠 onResume/onPause 两个来判断,在使用 Fragment Tab 时,tab 切换触发的回调是 onHiddenChanged 方法,而 ViewPager 中切换 Fragment 时触发的是 setUserVisibleHint,所以需要切入这四个方法。
@Pointcut("execution(void onHiddenChanged(boolean)) && within(android.support.v4.app.Fragment) && target(fragment) && args(hidden)")
public void onHiddenChanged(Fragment fragment, boolean hidden) {}
@Pointcut("execution(void setUserVisibleHint(..)) && within(android.support.v4.app.Fragment) && target(fragment) && args(visible)")
public void setUserVisibleHint(Fragment fragment, boolean visible) {}
@Pointcut("execution(void onResume()) && within(android.support.v4.app.Fragment) && target(fragment)")
public void onResume(Fragment fragment) {}
@Pointcut("execution(void onPause()) && within(android.support.v4.app.Fragment) && target(fragment)")
public void onPause(Fragment fragment) {}
Click 事件统计
这里只是以普通的 View 的点击事件统计为例,这里的点击事件是指点击某 View 后会执行点击回调这种,所以只需要关注 OnClickListener 即可。
很容易写出下面代码:
@Pointcut("execution(void android.view.View.OnClickListener.onClick(..)) && args(view)")
public void onClick(View view) {}
但是上面的 Pointcut 其实是不全面的,setOnClickListener(new OnClickListener() {…}) 这种匿名内部类写法时没问题,如果是实现 OnClickListener 接口的类则无法切入。所以应该加上 OnClickListener 的子类,使用 OnClickListener+
。
@Pointcut("execution(void android.view.View.OnClickListener+.onClick(..)) && args(view)")
public void onClick(View view) {}
这种写法还可以监听到 ButterKnife 的点击事件,因为 ButterKnife 使用一个实现了 OnClickListener 的抽象接口。
所以,从上面三个示例可以看出非侵入式 AspectJ 的特点:
- 不需要额外的自定义注解
- 不会侵入切入点代码
- 很难精确控制切入点,需要修改切入点时,必须修改切面代码
android-aspectjx 插件的常见问题
AspectJ 在 Android 项目中使用需要额外处理,在第一篇文章引入 AspectJ 有到使用的国人写开源插件 gradle_plugin_android_aspectjx,该插件利用了 Gradle 的 Transforms API 用 AspectJ tools 对所有 class 进行处理。
下面是个人在使用遇到的一些问题,总结下避免大家走弯路:
includeJarFilter 和 excludeJarFilter 的使用
默认不做任何配置的话,会遍历项目编译后所有的 .class 文件和依赖的第三方库进行处理。不过可以通过 includeJarFilter 和 excludeJarFilter 过滤缩小范围。
aspectjx {
//includes the libs that you want to weave
includeJarFilter 'universal-image-loader', 'AspectJX-Demo/library'
//excludes the libs that you don't want to weave
excludeJarFilter 'universal-image-loader'
}
编译过程中可以通过 gradle 输出看出使用 AspectJ 处理的第三方库:
aspect start..........
excludeJar:::XXXX
includeJar:::XXXX
.
.
.
aspect do work..........
aspect jar merging..........
aspect done...................
需要注意的切面代码和需要作为切入点的第三方库必须 include,如果切面代码是在 library module 中,没有 include 的话,运行时会出现找不到 aspectOf()
方法的异常。
切入点代码可以混淆
代码是在编译阶段织入,所以混淆是不会有影响的,只有在运行时你需要通过类名,方法名去做一些事情的时候才不能混淆,比如用到了反射技术等。任何在编译阶段植入代码的 AOP 方案混淆都不会受影响,和混淆无关。而在运行时的 AOP 方案就会受混淆影响。