小伙伴们在学习java基础的时候肯定都都会学面向对象思想和三大特性。在OOP设计中有个单一职责原则,在很多时候都不会有问题,但是当很多模块都需要同一个功能的时候,这个时候还用OOP就会很麻烦。比如我们希望在各个模块中打印log,所以我们写了一个log类CustomLog类,然后在需要的地方写上一句log代码,但是这样对模块间侵入太深,极大增加了模块间的耦合。这个时候我们可以了解一下AOP切面编程。AOP是Aspect-Oriented Progreming的缩写,它提倡的是针对同一类问题的统一处理,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。基于此,AOP在Android中的应用就应运而生了。
基于AOP特性,我们自然而然能够想到切面编程在android开发中非常多的应用场景,例如无感埋点,因为埋点的代码都很通用,所以我们可以自定义 @HookMethod注解用于埋点。也可以自定义异步注解,将方法放在子线程中去执行。还可以用于编写安全气囊,保障APP的稳定性。
应公司进一步降低APP奔溃率要求,我们决定使用AOP切面编程来给应用增加一个贴心的保护o(╯□╰)o。
AspectJ
就好像OOP中的Java一样,一些先行者也搞了一套东西来支持AOP。目前用得比较火的就是AspectJ了。它其实不是一个新的语言,它就是一个代码编译器,在Java编译器的基础上增加了一些它自己的关键字识别和编译方法。因此,ajc也可以编译Java代码。当然,除了使用AspectJ特殊的关键字外,只要加上对应的AspectJ注解就好。AspectJ在Android Studio中的配置这里就不赘述了,有兴趣的小伙伴可以查看这里,我这篇文章也是参考自这里。
基础概念
Aspect 切面:实现了cross-cutting功能,是针对切面的模块。最常见的是logging模块、方法执行耗时模块,这样,程序按功能被分为好几层,如果按传统的继承的话,商业模型继承日志模块的话需要插入修改的地方太多,而通过创建一个切面就可以使用AOP来实现相同的功能了,我们可以针对不同的需求做出不同的切面。
PointCut 切入点:pointcut可以控制你把哪些advice应用于jointpoint上去,通常你使用pointcuts通过正则表达式来把明显的名字和模式进行匹配应用。决定了那个jointpoint会获得通知。分为call、execution、target、this、within等关键字(具体含义见第四节)
Advice 通知:advice是我们切面功能的实现,它是切点的真正执行的地方。比如像写日志到一个文件中,advice(包括:before、after、around等)在jointpoint处插入代码到应用程序中。我们来看一看原AspectJ程序和反编译过后的程序。看完下面的图我们就大概明白了AspectJ是如何达到监控源程序的信息了。
Joint Point 连接点:连接点是切面插入应用程序的地方,该点能被方法调用,而且也会被抛出意外。连接点是应用程序提供给切面插入的地方,在插入地建立AspectJ程序与源程序的连接。
Weaving 编织:主要是在编译期使用AJC将切面的代码注入到目标中, 并生成出代码混合过的.class的过程.
当我们希望执行切面编程的时候,首先要找到切入点。也就是说我们要找到被操作的对象,我们用PointCut来过滤,找到切入点。找到切入点后,我们需要确定切面编程执行时机,这个时候需要试用通知Advice,下面来详细看一下:
@Aspect//试用AspectJ编译
public class MMHSafeAspect {
//环绕通知,在合适的时机手动执行被代理的方法
//synthetic关键字表示由编译器创造的方法,故忽略
@Around("execution(!synthetic * *(..)) && onSafe()")
public Object doSafeMethod(final ProceedingJoinPoint joinPoint) throws Throwable {
return safeMethod(joinPoint);
}
//找到需要切面编程的切入点
//CrashSafe为自定义注解,即注释了CrashSafe的方法都会被安全气囊保护
@Pointcut("@within(包名.CrashSafe)||@annotation(包名.CrashSafe)")
public void onSafe() {
}
private Object safeMethod(final ProceedingJoinPoint joinPoint) throws Throwable {
Object result = null;
try {
//手动执行被代理的方法,并在这里try_catch,起到安全气囊的作用
result = joinPoint.proceed(joinPoint.getArgs());
} catch (Throwable e) {
Log.e("MMHSafeAspect", "安全气囊拦截异常", e);
if(null != AopApplicationContext.getInstacne()){
AopApplicationContext.getInstacne().postBugly(e);
if(BuildConfig.DEBUG){
BugTipActivity.startBugTip(Log.getStackTraceString(e));
}
}
}
return result;
}
}
上面即安全气囊的核心代码,我们可以举一反三,自己建立一些基本数据结构,并且采用安全气囊保护。达到极大降低java层crash的目的。当然这只是增加用户体验的一种手段。我们不能用这个方法来逃避问题,所以应该在debug模式的时候暴露问题,在线上版本的时候才去启用安全气囊。回到主题,小伙伴们可能对Aspect,execution这些关键字觉得很陌生,别急,下面慢慢讲:
关键字
Aspect:这里要使用Aspect的编译器编译必须给类打上标注
execution: 顾名思义,它截获的是方法真正执行的代码区,Around方法块就是专门为它存在的。调用Around可以控制原方法的执行与否,可以选择执行也可以选择替换。与此类似的还有call, 同样,从名字可以看出,call截获的是方法的调用区,它并不截获代码真正的执行区域,它截获的是方法调用之前与调用之后(与before、after配合使用),在调用方法的前后插入JoinPoint和before、after通知。它截获的信息并没有execution那么多,它无法控制原来方法的执行与否,只是在方法调用前后插入切点,因此它比较适合做一些轻量的监控(方法调用耗时,方法的返回值等)。
Around:目标方法体被Around方法替换,原方法重新生成,名为XXX_aroundBody(),如果要调用原方法需要在AspectJ程序的Around方法体内调用joinPoint.proceed()还原方法执行,是这样达到替换原方法的目的。达到这个目的需要双方互相引用,桥梁便是Aspect类,目标程序插入了Aspect类所在的包获取引用。AspectJ通过在目标类里面加入Closure(闭包)类,该类构造函数包含了目标类实例、目标方法参数、JoinPoint对象等信息,同时该类作为切点原方法的执行代理,该闭包通过Aspect类调用Around方法传入Aspect程序。这样便达到了关联的目的,便可以在Aspect程序中监控和修改目标程序。
Before与After: Before与After只是在方法被调用前和调用之后添加JoinPoint和通知方法(直接插入原程序方法体中),调用AspectJ程序定义的Advise方法,它并不替代原方法,是在方法call之前和之后做一个插入操作。After分为returnning和throwing两类,前者是在正常returning之后调用,后者是在throwing发生之后调用。默认的After是在finally处调用,因此它包含了前面的两种情况。
其他常见的关键字还有:
this :用于匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口也类型匹配;
target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;
args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;
within:用于匹配指定类型内的方法执行;
使用方式
1.结合自定义注解使用
上面的代码就是使用这个方法,这是个混合用法,可以在execution、call中使用注解,然后该注解标注在目标方法上就可以实现关联,并且截获。这样做缺点是入侵了源码,一些轻量的可以这么操作。优点是灵活
2.直接使用
也就是说直接在写死切入点,这样做的缺点是范围太广,假设我们把所有的activity中的所有方法都设为切入点,岂不是非常损耗性能?
所以我们可以结合业务需求选择自己合适的使用方式。下班了,有时间接着写
网友评论