面向切面
AOP(Aspect Oriented Programming)即:面向切面编程,通过预编译(pre-compiled)方式和运行期动态代理实现程序功能统一维护的一种技术。而对于AspectJ而言其实就是选取代码中的一个某个共有
执行点
选取出来,并在一些特定的条件下织入我们的代码来完成编译插桩,以便完成特定逻辑,就比如登录状态检查、日志代码的织入和埋点等等,实际上AspectJ是在class->dex时对字节码织入了代码,对于修改字节码的还有ASM、Javassist等,而AspectJ是会在我们的代码中织入它封装好的代码,可能会对性能有轻微的影响,如果对性能要求高的话,建议使用ASM直接修改字节码而不会织入其他多余的代码,这些我们这里不多说,感兴趣的可以网上有很多文章介绍,关于AspectJ的介绍我在性能优化之启动优化(一)这篇有大体的介绍。关于操作字节码的库有很多感兴趣的可以去了解一下。
1、Join Point
JoinPoints(连接点),程序中可能作为代码注入(织入)目标的特定的点,一般在代码中可能会存在很多Join Point(连接点)。在AspectJ中可以作为JoinPoints的地方包括:
Join Point | 说明 |
---|---|
Method call | 方法被调用,即在方法调用处织入代码 |
Method execution | 方法执行处,即在方法体重中织入代码 |
Constructor call | 构造函数被调用 |
Constructor execution | 构造函数执行 |
Field get | 读取属性 |
Field set | 写入属性 |
Pre-initialization | 与构造函数有关,很少用到 |
Initialization | 与构造函数有关,很少用到 |
Static initialization | static 块初始化 |
Handler | 异常处理 |
Advice execution | 所有 Advice 执行 |
PointCut(切入点)
Pointcut 切入点,即一组Join Point(连接点)的集合,PointCuts就是Join Point的集合,只是说PointCut是具有条件的 Join Point ,在程序中可能存在很多Join Point,那么我们就需要通过Pointcut去筛选出我们感兴趣的Join Point来做处理。
Join Point | Pointcuts 语法 |
---|---|
Method call | call(MethodPattern) |
Method execution | execution(MethodPattern) |
Constructor call | call(ConstructorPattern) |
Constructor execution | execution(ConstructorPattern) |
Field get | get(FieldPattern) |
Field set | set(FieldPattern) |
Pre-initialization | initialization(ConstructorPattern) |
Initialization | preinitialization(ConstructorPattern) |
Static initialization | staticinitialization(TypePattern) |
Handler | handler(TypePattern) |
Advice execution | adviceexcution() |
Pattern就是正则的意思,即MethodPattern表示匹配方法的正则,上面除了 Join Point 对应的切点,Pointcuts 还有其他选择方法:
-
within(TypePattern) TypePattern 表示某个包或类中包含JPoint,符合 TypePattern 的代码中的 Join Point,表示在com.xxx.XxxActivity类型中的Join Point:
@Pointcut("!within(com.aop.XxxActivity)") public void baseCondition() {}
-
this(Type) Join Point 所属的 this 对象是否 instanceOf Type的类型,即就是说被织入代码的Type是否Type类型的实例;
@Pointcut("!this(com.xxx.xxx.aop.*) && !this(com.android.xxx.activity.*)") public void baseCondition() {}
-
target(Type) Join Point 所在的对象(例如 call 或 execution 操作符应用的对象)是否 instanceOf Type的类型,表示对com.xxx.MyHttpClient类型的对象;
@Pointcut("!target(com.xxx.MyHttpClient)") public void baseCondition() {}
-
args(Type , ...) 方法或构造函数参数的类型,如;arges(long,..),对Join Point的参数进行条件筛选,下面的是表示对参数是request 的Join Point。
@Pointcut("call(org.apache.http.HttpResponse org.apache.http.client.HttpClient.execute(org.apache.http.client.methods.HttpUriRequest)) && (target(httpClient) && (args(request) && baseCondition()))") public void httpClientExecuteOne(HttpClient httpClient, HttpUriRequest request) {}
target 与 this区别?
public class PersonAspect{
@Pointcut("call(* com.xx.Person.eat(..))")
public void callMethod() {
Log.e("tag", "callMethod->");
}
@Around("callMethod()")
public void beforeMethodCall(JoinPoint joinPoint) {
Log.e("tag", "getTarget->" + joinPoint.getTarget());
Log.e("tag", "getThis->" + joinPoint.getThis());
}
}
public class MainActivity extends Activity{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Person person= new Person();
person.eat();
}
}
getTarget-> com.xx.Person@68b1c9g
getThis->com.xx..MainActivity@48d38bd
- target 与 this 很容易混淆,target指的是切入点的所有者,而this指代的是被织入代码所属类的实例对象。
- 如果把call 换成 execution 结果是这样的:
getTarget-> com.xx.Person@68b1c9g
getThis->com.xx.Person@68b1c9g
call和execution的区别
就是说execution是在切点处出织入代码,比如织入函数,那么execution织入的是函数体中,而call织入的是函数调用处。
Before 、After和Around区别
@Aspect
public class TestAspect {
private static boolean runAround = true;
public static void main(String[] args) {
new TestAspect().hello();
runAround = false;
new TestAspect().hello();
}
public void hello() {
System.err.println("in hello");
}
@After("execution(void aspects.TestAspect.hello())")
public void afterHello(JoinPoint joinPoint) {
System.err.println("after " + joinPoint);
}
@Around("execution(void aspects.TestAspect.hello())")
public void aroundHello(ProceedingJoinPoint joinPoint) throws Throwable {
System.err.println("in around before " + joinPoint);
if (runAround) {
joinPoint.proceed();
}
System.err.println("in around after " + joinPoint);
}
@Before("execution(void aspects.TestAspect.hello())")
public void beforeHello(JoinPoint joinPoint) {
System.err.println("before " + joinPoint);
}
}
输出日志:
in around before execution(void aspects.TestAspect.hello())
before execution(void aspects.TestAspect.hello())
in hello
after execution(void aspects.TestAspect.hello())
in around after execution(void aspects.TestAspect.hello())
in around before execution(void aspects.TestAspect.hello())
in around after execution(void aspects.TestAspect.hello())
当你使用around 时,如果你不调用joinPoint.proceed()
该方法,那么被织入的切点函数或其他切点不会被调用,并且Befor和After是不可以使用ProceedingJoinPoint
作为参数,只能使用JoinPoint 作为参数。around 只能使用execution不能使用call。
Pointcuts 的语法中涉及到一些 Pattern,下面是这些 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 涉及到的类型规则也是一样,可以使用 '!'、''、'..'、'+','!' 表示取反,'' 匹配除 . 外的所有字符串,'*' 单独使用事表示匹配任意类型,'..' 匹配任意字符串,'..' 单独使用时表示匹配任意长度任意类型,'+' 匹配其自身及子类,还有一个 '...'表示不定个数。 |
Advice
Advice | 说明 |
---|---|
@Before | 在执行 Join Point 之前 |
@After | 在执行 Join Point 之后,包括正常的 return 和 throw 异常 |
@AfterReturning | Join Point 为方法调用且正常 return 时,不指定返回类型时匹配所有类型 |
@AfterThrowing Join Point | 为方法调用且抛出异常时,不指定异常类型时匹配所有类型 |
@Around | 替代 Join Point 的代码,如果要执行原来代码的话,要使用 ProceedingJoinPoint.proceed() |
关于AspectJ,本人推荐使用沪江gradle_plugin_android_aspectjx的插件,在Aspectj的基础上,还支持Kotlin。
进入主题先来分析我们应该如何完成这个需求:
- 首先登录状态在整个工程中有很多的地方会使用到,那么我们是否可以把代码抽取为接口?
- 那么我们写得这个只是检查登录的状态吗?当然不是,我们还可能检查网络状态。
- 那么上面的需求是不是就完了呢?不是的,在我们的程序中可能存在很多切点(Point Cut),我们不可能每个都做检查,这样不现实,所以我们需要打标志,哪些是需要我们AspectJ做处理织入代码的Point Cut,所以我们可以使用注解(Annotation)。
定义接口:
interface CheckStatus {
/**
* 检查状态
*
* @return true表示检查通过,false表示检查不通过
*/
fun doCheck(context: Context?): Boolean
}
我们定义了CheckStatus 接口,具体实现由子类完成,反正我只需要知道你给我返回的状态是否可用就行。
为了让AspectJ能能够精确切点(Point Cut)的位置,我们还需要定义注解(Annotation),对切点(Point Cut)进行筛选,筛选出我们感兴趣的切点:
@kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
annotation class Check(vararg val value: KClass<out CheckStatus>)
可能有人会有疑问,注解为什么是可变参数,那是因为检查状态可能会同时检查多个状态,如同时检查登录和网络,一般只有这些条件都满足了才会通过检查,当然条件的优先级,也是和我们传入的class顺序相关,接下来就进入我们的切面相关的代码了。
@Aspect
@SuppressWarnings("unused")
class CheckStatusAspect {
// 使用WeakHashMap缓存起来,防止每次调用方法都要反射
private val mCacheStatusClass by lazy {
WeakHashMap<KClass<out CheckStatus>, CheckStatus>()
}
//定义切面的规则
//1.就在原来应用中哪些注释的地方放到当前切面进行处理,筛选切点
//execution(注解名 注解用的地方) ,其他类型的参数使用ars
//方法名自己定义
@Pointcut("execution(@com.youbesun.perform.aop.Check * *(..))")
fun checkStatus() {
}
//2.对进入切面的内容如何处理
//advice
//@Before() 在切入点之前运行
//@After() 在切入点之后运行
//@Around() 在切入点前后都运行
//方法名自己定义
@Around("checkStatus()")
@Throws(Throwable::class)
@SuppressWarnings("unused")
fun aroundJointPoint(joinPoint: ProceedingJoinPoint) {
//初始化context
val context = when (val obj = joinPoint.getThis()) {
is Context -> obj
is androidx.fragment.app.Fragment -> obj.activity
is android.app.Fragment -> obj.activity
else -> {
LogHelper.e("AOP IS Around Joint Point checkStatus Error !")
joinPoint.proceed()
return
}
}
//最后判断这个方法中的注解,是否都满足条件
if (createInstance(joinPoint, context)) joinPoint.proceed()
}
private fun createInstance(
joinPoint: ProceedingJoinPoint,
context: Context?
): Boolean {
//是否满足条件
var isCheckSuccess = false
//获取方法信息
val methodSignature = joinPoint.signature as MethodSignature
val statusKClass = methodSignature.method.getAnnotation(Check::class.java).value
/*可能需要检查多个条件,如:登录和网络,只有全部成立才会通过*/
statusKClass.forEach { it ->
var statusCheck = mCacheStatusClass[it]
if (statusCheck == null) {
tryCatch({
//反射创建实现类实例
statusCheck = it.constructors.asSequence().firstOrNull()?.call()
// 优化点,使用缓存避免同一个实例重复反射,造成性能损耗
if (statusCheck != null) mCacheStatusClass[it] = statusCheck
}, {
// 创建实例有问题,上交bugly
StabilityHelper.postCatcherException(
RuntimeException("CheckStatusAspect: ${it.message}", it)
)
})
}
isCheckSuccess = statusCheck?.doCheck(context) ?: false
//优化点,如果有存在条件false立即停止
if (!isCheckSuccess) return isCheckSuccess
}
return isCheckSuccess
}
}
代码中的每一步都有清晰的注释,这里就不多说的,定义注解是为了减少切点的筛选,这里唯一需要注意的是,我么定义的接口和它的实现类不能被混淆,因为这里用到了反射,使用就非常的简单,代码如下:
/**
* @Describe:登录状态检查
*/
class CheckLoginStatus : CheckStatus {
override fun doCheck(context: Context?): Boolean {
val isLogin = Account.isLogin()
if (!isLogin) context?.let { it.startActivity(it.intentFor<LoginActivity>()) }
return isLogin
}
}
然后在指定的方法上使用注解:
@Check(CheckLoginStatus::class)
override fun onClick(v: View) {}
这段代码就是检查登录状态的,当然你可以在实现一个检查网络的,比如:
@Check(CheckNetWorkStatus::class,CheckLoginStatus::class)
override fun onClick(v: View) {}
这段代码会优先检查网络状态,如果网络可用接着在检查登录状态,但是如果第一个条件网络状态没有通过,那么直接就不会下一步检查。到这里AspectJ就结束了,下一篇将会使用ASM替换AspectJ,使用gardle transform + asm对代码进行织入。。最后对于面向切面编程说两句,aop对目前在日志统计、埋点等等使用的非常多,可以做很多事,最近在看前微信大佬张绍文的Android开发高手课,发现原来我学的是假的Android。