AOP(Aspect Oriented Programming)面向切面编程,我们知道有OOP面向对象编程,提倡功能模块化,为啥要学习AOP呢,本文将从是什么、为什么、怎么用来介绍AOP,看完本文你一定会对AOP有一个完整的认识。
面向切面编程,顾名思义就是以切入的方式实现某些功能,我们在实现某些功能,诸如日志埋点、性能统计、防抖、权限控制、为了减少对原有代码的入侵,采用这种编程方法。
我们先看看不使用AOP的方式实现一个统计方法耗时,代码如下:
val beginTime = SystemClock.currentThreadTimeMillis()
// 处理业务逻辑
dosomething();
val endTime = SystemClock.currentThreadTimeMillis()
val dx = endTime - beginTime
Log.e("ddup", methodSignature.name + "方法执行耗时:" + dx + "ms")
如果我们有多个方法需要统计,我们需要每个方法加上这样的代码,这样看起来是不是有很多重复代码,而且对我们原来的业务代码入侵,造成破坏。我们有没有一种打点的方式,类似注解的方式,将需要统计方法添加一个注解,最后再统一处理,类似下面的代码:
@AopMethodTime
fun loadData() {
for (index in 1..1000) {
Log.e("ddup", "load data..." + index)
}
}
需要统计的方法,只需要添加@AopMethodTime,是不是简单多了,这就是AOP魅力所在。
了解AOP,我们需要了解一下几个基本概念:
这些概念有些晦涩难懂,说实话我第一次接触AOP看一堆术语,也觉得麻烦,不过不用担心,有个印象就行,自己动手实现一个例子就没有问题了。
介绍完AOP是什么,我们来说说常用的AOP技术,从织入的时机来看,分为静态织入和动态织入。
主要的技术框架如下:
织入时机 | 技术框架 |
---|---|
静态织入 | APT,AspectJ、ASM、Javassit |
动态织入 | Java动态代理,cglib、Javassit |
下面简单的介绍上面的技术框架:
APT(Annotation Processing Tool)即注解处理器,它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理,简单的来说,它就是帮助我们扫描注解并生成java代码工具的技术,我们熟悉的Butterknife也是基于此技术实现的,感兴趣可以之前我之前写的一篇文章手把手教你实现一个Butterknife
AspectJ是一种严格意义上的AOP技术,因为它提供了完整的面向切面编程的注解,这样让使用者可以在不关心字节码原理的情况下完成代码的织入,因为编写的切面代码就是要织入的实际代码,我介绍的技术也是基于此,Hugo统计方法耗时的框架也是基于此。
ASM是非常底层的面向字节码编程的AOP框架,理论上可以实现任何关于字节码的修改,非常硬核。许多字节码生成API底层都是用ASM实现,常见比如Groovy、cglib,因此在Android平台下使用ASM无需添加额外的依赖。完整的学习ASM必须了解字节码和JVM相关知识。
javassit是一个开源的字节码创建、编辑类库,现属于Jboss web容器的一个子模块,特点是简单、快速,与AspectJ一样,使用它不需要了解字节码和虚拟机指令。
JDK本身的一种技术啊,是代理模式的一种实现,运行时动态增强原始类的行为,实现方式是运行时直接生成class字节码并将其加载进虚拟机,我们在使用Android hook技术会经常用到它,具体使用,这里我就不介绍了。
CGLIB是一个强大的、高性能的代码生成库。其被广泛应用于AOP框架(Spring、dynaop)中,用以提供方法拦截操作。代码库地址:https://github.com/cglib/cglib
上述技术部分介绍引自谈谈Android AOP技术方案
介绍完相关技术后,下面通过几个完整例子来实际体验一下AOP技术。由于最近一直在学习kotlin,以下基于kotlin实现,java代码实现,下面会介绍一个开源库啊。
Project build.gradle引入库:
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.10'
app build.gradle引入库:
implementation "org.aspectj:aspectjrt:1.9.5"
定义一个快速点击注解:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION)
annotation class AopOnClick(
val value: Long = 2000
)
定义一个快速点击的切面aspect:
/**
* 定义切点,标记切点为所有被@AopOnclick注解的方法
*/
@Pointcut("execution(@com.example.android_aop.annotation.AopOnClick * *(..))")
fun methodClick() {
}
/**
* 定义一个切面方法,包裹切点方法
*/
@Around("methodClick()")
@Throws(Throwable::class)
fun aroundClickJoinPoint(joinPoint: ProceedingJoinPoint) {
// 取出方法的注解
val methodSignature = joinPoint.signature as MethodSignature
val method = methodSignature.method
if (!method.isAnnotationPresent(AopOnClick::class.java)) {
return
}
val aopOnclick = method.getAnnotation(AopOnClick::class.java)
var view: View? = null;
for (arg in joinPoint.args) {
if (arg is View) {
view = arg
break
}
}
// 判断是否快速点击
if (view != null && !AopClickUtils.isFastDoubleClick(view, aopOnclick.value)) {
// 不是快速点击,执行原方法
joinPoint.proceed()
}
}
这里主要说一下execution(@com.example.android_aop.annotation.AopOnClick * *(…)),这个是我们要切入的注解,这个地址要写对,要不能会报错,aopOnclick.value是我们使用地方传入的值,**joinPoint.proceed()**是原来方法的执行,我们可以控制是否需要拦截,joinPoint参数可以获取注解方法里面的参数。
防抖的处理AopClickUtils:
/**
* 最近一次点击时间
*/
private var mLastClickTime: Long = 0
/**
* 最近一次点击的控件ID
*/
private var mLastClickViewId: Int = 0
/**
* 是否快速点击
*/
fun isFastDoubleClick(v: View, intervalMillis: Long): Boolean {
var time = System.currentTimeMillis();
var timeInterval = time - mLastClickTime
val viewId = v.id
if (timeInterval > 0 && timeInterval < intervalMillis && viewId == mLastClickViewId) {
return true
} else {
mLastClickTime = time
mLastClickViewId = viewId;
return false
}
}
调用如下:
/**
* 快速点击
*/
@AopOnClick(1000)
fun noDoubleClick(v: View?) {
Log.e("ddup", "noDoubleClick...")
}
我们在需要防止快速点击的方法,添加如下@AopOnClick(1000)注解,1000代表一秒执行一次,打印一次noDoubleClick…日志
申请权限:
/**
* 申请权限
*/
@AopPermission(
[PermissionConsts.CALENDAR, PermissionConsts.CAMERA, PermissionConsts.LOCATION]
)
private fun requestPermissions() {
Log.e("ddup", "requestPermissions success...")
}
添加上面代码可以就可以申请日历、相机、定位的权限
切换线程:
/**
* 改变线程
*/
@AopOnClick()
@AopIOThread(ThreadType.Single)
private fun changeThread(v: View?) {
Log.e("ddup", "current thread = " + Thread.currentThread().name.toString())
}
添加上面代码可以就可以切换到单线程实现的子线程里面
参考例子地址:github地址,觉得不错的给个。
java实现:大神写的一个aop框架
至此已全部介绍完了AOP相关知识,从上面的例子可以看出,AOP对我们Android在实现某些功能,对原有代码几乎无侵入性,使用简单,首先我们定义一个注解,需要调用的地方加上注解,而且我们可以对同一个方法添加多个注解,切入多次,然后在定义一个切面,实现我们要切入的内容,并且可以选择是否拦截原有方法,最后再需要切入的地方添加注解即可。
当然AOP也不是十分完美的,出现问题相比一般的错误,定位相对要更加麻烦,基于AspectJ的实现方式会存在重复织入、不织入的问题。
最后,我想说的是AOP不乏是一种很好的技术,我们可以在实现某些功能,尝试使用它,一定会让我们事半功倍。
Thanks
谈谈Android AOP技术方案
Android面向切面编程(AOP)
XAOP
创作不易,觉得不错的话,请点赞、评论鼓励,谢谢。