Android AspectJ详解

}

①处声明了本类是一个AspectJ配置文件。

②处指定了一个代码织入点,注解内的call(* com.wandering.sample.aspectj.Animal.fly(…)) 是一个切点表达式,第一个*号表示返回值可为任意类型,后跟包名+类名+方法名,括号内表示参数列表, 表示匹配任意个参数,参数类型为任何类型,这个表达式指定了一个时机:在Animal类的fly方法被调用时。

③处声明Advice类型为Before并指定切点为上面callMethod方法所表示的那个切点。

④处为实际织入的代码。

翻译成白话就是说在Animal类的fly方法被调用前插入④处的代码。

编写测试代码并调用fly方法,运行观察日志输出你会发现before->的日志先于animal fly日志被打印,具体可查看sample工程MethodAspect示例。

我们再将APK反编译看一下织入结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

红色框选部分就是AspectJ为我们织入的代码。

通过上面的例子我们了解了AspectJ的基本用法,但实际上AspectJ的语法可以十分复杂,下面我们来看看具体的语法。

Join Point

上面的例子中少讲了一个连接点的概念,连接点表示可织入代码的点,它属于Pointcut的一部分。由于语法内容较多,实际使用过程中我们可以参考语法手册,我们列出其中一部分Join Point:

Joint Point含义
Method call方法被调用
Method execution方法执行
Constructor call构造函数被调用
Constructor execution构造函数执行
Static initializationstatic 块初始化
Field get读取属性
Field set写入属性
Handler异常处理

Method call 和 Method execution的区别常拿来比较,其实就是调用与执行的区别,就拿上面Animal的fly方法举例。demo代码如下:

Animal a = Animal();
a.fly();

如果我们声明的织入点为call,再假设Advice类型是before,则织入后代码结构是这样的。

Animal a = new Animal();
//…我是织入代码
a.fly();

如果我们声明的织入点为execution,则织入后代码结构就成这样了。

public class Animal {
public void fly() {
//…我是织入代码
Log.e(TAG, “animal fly method:” + this.toString() + “#fly”);
}
}

本质上的区别就是织入对象不同,call被织入在指定方法被调用的位置上,而execution被织入到指定的方法内部。

Pointcut

Pointcuts是具体的切入点,基本上Pointcuts 是和 Join Point 相对应的。

Joint PointPointcuts 表达式
Method callcall(MethodPattern)
Method executionexecution(MethodPattern)
Constructor callcall(ConstructorPattern)
Constructor executionexecution(ConstructorPattern)
Static initializationstaticinitialization(TypePattern)
Field getget(FieldPattern)
Field setset(FieldPattern)
Handlerhandler(TypePattern)

除了上面与 Join Point 对应的选择外,Pointcuts 还有其他选择方法。

Pointcuts 表达式说明
within(TypePattern)符合 TypePattern 的代码中的 Join Point
withincode(MethodPattern)在某些方法中的 Join Point
withincode(ConstructorPattern)在某些构造函数中的 Join Point
cflow(Pointcut)Pointcut 选择出的切入点 P 的控制流中的所有 Join Point,包括 P 本身
cflowbelow(Pointcut)Pointcut 选择出的切入点 P 的控制流中的所有 Join Point,不包括 P 本身
this(Type or Id)Join Point 所属的 this 对象是否 instanceOf Type 或者 Id 的类型
target(Type or Id)Join Point 所在的对象(例如 call 或 execution 操作符应用的对象)是否 instanceOf Type 或者 Id 的类型
args(Type or Id, …)方法或构造函数参数的类型
if(BooleanExpression)满足表达式的 Join Point,表达式只能使用静态属性、Pointcuts 或 Advice 暴露的参数、thisJoinPoint 对象
this vs. target

this和target是一个容易混淆的点。

MethodAspect.java

public class MethodAspect {
@Pointcut(“call(* com.wandering.sample.aspectj.Animal.fly(…))”)
public void callMethod() {
Log.e(TAG, “callMethod->”);
}

@Before(“callMethod()”)
public void beforeMethodCall(JoinPoint joinPoint) {
Log.e(TAG, “getTarget->” + joinPoint.getTarget());
Log.e(TAG, “getThis->” + joinPoint.getThis());
}
}

fly调用方:

MainActivity.java

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Animal animal = new Animal();
animal.fly();
}

运行结果如下:

getTarget->com.wandering.sample.aspectj.Animal@509ddfd
getThis->com.wandering.sample.aspectj.MainActivity@98c38bf

也就是说target指代的是切入点方法的所有者,而this指代的是被织入代码所属类的实例对象。

我们稍加改动,将切点的call改为execution。

运行结果就成这个样子了:

getTarget->com.wandering.sample.aspectj.Animal@509ddfd
getThis->com.wandering.sample.aspectj.Animal@509ddfd

按照上面的分析,与这个结果也是吻合的。

条件运算

Pointcut表达式中还可以使用一些条件判断符,比如 !、&&、||。

以Hugo为例:

Hugo.java

@Pointcut(“within(@hugo.weaving.DebugLog *)”)
public void withinAnnotatedClass() {}

@Pointcut(“execution(!synthetic * *(…)) && withinAnnotatedClass()”)
public void methodInsideAnnotatedType() {}

第一个切点指定范围为包含DebugLog注解的任意类和方法,第二个切点为在第一个切点范围内,且执行非内部类的任意方法。结合起来表述就是任意声明了DebugLog注解的方法。

其中@hugo.weaving.DebugLog *!synthetic * *(..)分别对应上面表格中提到的TypePattern和MethodPattern。

接下来需要了解这些pattern具体的语法,通过语法我们可以写出符合自身需求的表达式。

Pattern类型语法
MethodPattern[!] [@Annotation] [public,protected,private] [static] [final] 返回值类型 [类名.]方法名(参数类型列表) [throws 异常类型]
ConstructorPattern[!] [@Annotation] [public,protected,private] [final] [类名.]new(参数类型列表) [throws 异常类型]
FieldPattern[!] [@Annotation] [public,protected,private] [static] [final] 属性类型 [类名.]属性名
TypePattern其他 Pattern 涉及到的类型规则也是一样,可以使用 ‘!’、‘’、‘…’、‘+’,‘!’ 表示取反,‘’ 匹配除 . 外的所有字符串,‘*’ 单独使用事表示匹配任意类型,‘…’ 匹配任意字符串,‘…’ 单独使用时表示匹配任意长度任意类型,‘+’ 匹配其自身及子类,还有一个 '…'表示不定个数

更多语法参见官网Pointcuts,非常有用。

再看几个例子:

execution(void setUserVisibleHint(…)) && target(android.support.v4.app.Fragment) && args(boolean) — 执行 Fragment 及其子类的 setUserVisibleHint(boolean) 方法时。

execution(void Foo.foo(…)) && cflowbelow(execution(void Foo.foo(…))) — 执行 Foo.foo() 方法中再递归执行 Foo.foo() 时。

if条件

通常情况下,Pointcuts注解的方法参数列表为空,返回值为void,方法体也为空。但是如果表达式中声明了:

  • args、target、this等类型参数,则可额外声明参数列表。
  • if条件,则方法必须public static boolean

来看sample示例MethodAspect8:

@Aspect
public class MethodAspect8 {
@Pointcut(“call(boolean .(int)) && args(i) && if()”)
public static boolean someCallWithIfTest(int i, JoinPoint jp) {
// any legal Java expression…
return i > 0 && jp.getSignature().getName().startsWith(“setAge”);
}

@Before(“someCallWithIfTest(i, jp)”)
public void aroundMethodCall(int i, JoinPoint jp) {
Log.e(TAG, "before if ");
}

}

切点方法someCallWithIfTest声明的注解表示任意方法,此方法返回值为boolean,参数签名为仅一个int类型的参数,后面跟上if条件,表示此int参数值大于0,且方法签名以setAge开头。

如此一来切面代码的执行就具备了动态性,但不是说不满足if条件的切点就不会织入代码。依然会织入,只是在调用织入代码前会执行someCallWithIfTest方法,当返回值为true时才会执行织入代码,下图是反编译class的结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

了解了原理后,实际上if逻辑也完全可以放到织入点代码中,理解起来会更容易一些。

Advice

直译过来是通知,实际上表示一类代码织入位置,在AspectJ中有五种类型的注解:Before、After、AfterReturning、AfterThrowing、Around,我们将它们统称为Advice注解。

Advice说明
@Before切入点前织入
@After切入点后织入,无论连接点执行如何,包括正常的 return 和 throw 异常
@AfterReturning只有在切入点正常返回之后才会执行,不指定返回类型时匹配所有类型
@AfterThrowing只有在切入点抛出异常后才执行,不指定异常类型时匹配所有类型
@Around替代原有切点,如果要执行原来代码的话,调用 ProceedingJoinPoint.proceed()

Advice注解修饰的方法有一些约束:

  1. 方法必须为public。
  2. Before、After、AfterReturning、AfterThrowing 四种类型方法返回值必须为void。
  3. Around的目标是替代原切入点,它一般会有返回值,这就要求声明的返回值类型必须与切入点方法的返回值保持一致;不能和其他 Advice 一起使用,如果在对一个 Pointcut 声明 Around 之后还声明 Before 或者 After 则会失效
  4. 方法签名可以额外声明JoinPoint、JoinPointStaticPart、JoinPoint.EnclosingStaticPart。

JoinPoint、JoinPointStaticPart、JoinPoint.EnclosingStaticPart又是什么呢?

在执行切面代码时,AspectJ会将连接点处的上下文信息封装成JoinPoint供我们使用。这些信息中有些是在编译阶段就可以确定的,比如方法签名 joinPoint.getSignature(),JoinPoint类型 joinPoint.getKind(),切点代码位置类名+行数joinPoint.getSourceLocation() 等等,我们将他们统称为JoinPointStaticPart。

而还有一些是在运行时才能确定的,比如前文提到的this、target、实参等等。

  • JoinPoint 包含连接点处的静态信息+动态信息。
  • JoinPointStaticPart 连接点处的静态信息。
  • EnclosingStaticPart 包含了连接点的静态信息,也就是连接点的上下文。

如果不需要动态信息,建议使用静态类型的参数,以提高性能。

讲了这么多理论,看起来比较复杂,实际上我们日常开发中的场景要相对简单一些。

常用示例

  1. 为所有点击事件埋点

@Aspect
public class MethodAspect5 {
@Pointcut(“execution(* android.view.View.OnClickListener+.onClick(…))”)
public void callMethod() {
}

@Before(“callMethod()”)
public void beforeMethodCall(JoinPoint joinPoint) {
Log.e(TAG, “埋点”);
}
}

android.view.View.OnClickListener+表示OnClickListener及其子类。

  1. 偷天换日,MethodAspect3使用Around类型的Advice,将调用run方法前将实参除以10后执行。

@Aspect
public class MethodAspect3 {

@Pointcut(“execution(* com.wandering.sample.aspectj.Animal.run(…))”)
public void callMethod() {
}

@Around(“callMethod()”)
public void aroundMethodCall(ProceedingJoinPoint joinPoint) {
//获取连接点参数列表
Object[] args = joinPoint.getArgs();
int params = 0;
for (Object arg : args) {
params = (int) arg / 10;
}
try {
//改变参数 执行连接点代码
joinPoint.proceed(new Object[]{params});值
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}

Around方法声明ProceedingJoinPoint类型而不是JoinPoint,可以使用其proceed方法调用连接点代码。

AspectJ现存的问题

重复织入、不织入

假如我们想对Activity生命周期织入埋点统计,我们可能写出这样的切点代码。

@Pointcut(“execution(* android.app.Activity+.on*(…))”)
public void callMethod() {}

由于Activity.class不参与打包(android.jar位于android设备内),参与打包是那些支持库比如support-v7中的AppCompatActivity,还有项目里定义的Activity,这就导致:

  1. 如果我们业务Activity中如果没有复写生命周期方法将不会织入。
  2. 如果我们的Activity继承树上如果都复写了生命周期方法,那么继承树上的所有Activity都会织入统计代码,这会导致重复统计。

解决办法是项目内定义一个基类Activity(比如BaseActivity),然后复写所有生命周期方法,然后将切点代码精确到这个BaseActivity。

@Pointcut(“execution(* com.xxx.BaseActivity.on*(…))”)
public void callMethod() {}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

想要了解更多关于大厂面试的同学可以点赞支持一下,除此之外,我也分享一些优质资源,包括:Android学习PDF+架构视频+源码笔记高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 这几块的内容。非常适合近期有面试和想在技术道路上继续精进的朋友。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

帮助,可以扫码获取!!(备注:Android)**

[外链图片转存中…(img-POJe0wgK-1713711956921)]

最后

想要了解更多关于大厂面试的同学可以点赞支持一下,除此之外,我也分享一些优质资源,包括:Android学习PDF+架构视频+源码笔记高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 这几块的内容。非常适合近期有面试和想在技术道路上继续精进的朋友。

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值