}
①处声明了本类是一个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 initialization | static 块初始化 |
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 Point | Pointcuts 表达式 |
---|---|
Method call | call(MethodPattern) |
Method execution | execution(MethodPattern) |
Constructor call | call(ConstructorPattern) |
Constructor execution | execution(ConstructorPattern) |
Static initialization | staticinitialization(TypePattern) |
Field get | get(FieldPattern) |
Field set | set(FieldPattern) |
Handler | handler(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注解修饰的方法有一些约束:
- 方法必须为public。
- Before、After、AfterReturning、AfterThrowing 四种类型方法返回值必须为void。
- Around的目标是替代原切入点,它一般会有返回值,这就要求声明的返回值类型必须与切入点方法的返回值保持一致;不能和其他 Advice 一起使用,如果在对一个 Pointcut 声明 Around 之后还声明 Before 或者 After 则会失效。
- 方法签名可以额外声明JoinPoint、JoinPointStaticPart、JoinPoint.EnclosingStaticPart。
JoinPoint、JoinPointStaticPart、JoinPoint.EnclosingStaticPart又是什么呢?
在执行切面代码时,AspectJ会将连接点处的上下文信息封装成JoinPoint供我们使用。这些信息中有些是在编译阶段就可以确定的,比如方法签名 joinPoint.getSignature(),JoinPoint类型 joinPoint.getKind(),切点代码位置类名+行数joinPoint.getSourceLocation() 等等,我们将他们统称为JoinPointStaticPart。
而还有一些是在运行时才能确定的,比如前文提到的this、target、实参等等。
- JoinPoint 包含连接点处的静态信息+动态信息。
- JoinPointStaticPart 连接点处的静态信息。
- EnclosingStaticPart 包含了连接点的静态信息,也就是连接点的上下文。
如果不需要动态信息,建议使用静态类型的参数,以提高性能。
讲了这么多理论,看起来比较复杂,实际上我们日常开发中的场景要相对简单一些。
常用示例
- 为所有点击事件埋点
@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及其子类。
- 偷天换日,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,这就导致:
- 如果我们业务Activity中如果没有复写生命周期方法将不会织入。
- 如果我们的Activity继承树上如果都复写了生命周期方法,那么继承树上的所有Activity都会织入统计代码,这会导致重复统计。
解决办法是项目内定义一个基类Activity(比如BaseActivity),然后复写所有生命周期方法,然后将切点代码精确到这个BaseActivity。
@Pointcut(“execution(* com.xxx.BaseActivity.on*(…))”)
public void callMethod() {}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
想要了解更多关于大厂面试的同学可以点赞支持一下,除此之外,我也分享一些优质资源,包括:Android学习PDF+架构视频+源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 这几块的内容。非常适合近期有面试和想在技术道路上继续精进的朋友。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
帮助,可以扫码获取!!(备注:Android)**
[外链图片转存中…(img-POJe0wgK-1713711956921)]
最后
想要了解更多关于大厂面试的同学可以点赞支持一下,除此之外,我也分享一些优质资源,包括:Android学习PDF+架构视频+源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 这几块的内容。非常适合近期有面试和想在技术道路上继续精进的朋友。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!