AspectJ in Android (三),AspectJ 两种用法以及常见问题

AspectJ in Android 系列:

AspectJ in Android (一),AspectJ 基础概念

AspectJ in Android (二),AspectJ 语法

AspectJ in Android (三),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 方法就无法作为切入点。

我觉得有两种方式比较合理:

  1. 项目中有个 BaseActivity 作为所有 Activity 的基类,而且 BaseActivity 重写了 Activity 的所有 onXX 方法时,可以以 BaseActivity 作为切入点。

Pointcut 表达式:execution(* BaseActivity.on**(..))

  1. 使用 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 方案就会受混淆影响。

暂不支持 Java 9 以上平台

其他问题请看插件的文档和 Issues。

AspectJ 系列文章到此就结束了,有什么疑问和建议欢迎在下面留言。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容