Android AOP编程(二)——AspectJ语法&实战

22 篇文章 1 订阅
本文介绍了AspectJ在Android中的使用,特别是如何利用它解决快速点击导致重复触发事件的问题。通过创建自定义注解@ClickOnce和相应的切入点,实现了在600毫秒内只允许一次点击的逻辑,避免了侵入原有代码并提高了灵活性。
摘要由CSDN通过智能技术生成

在上一篇Android AOP编程(一)——AspectJ基础知识中我记录了在Android中使用AspectJ实现AOP编程的一些基础知识,但是AspectJ的使用其实最主要的是针对切面的语法,找切面并不难,难的是如何编写匹配这个切面的规则,本篇主要记录的就是AspectJ的语法,以及使用一个实例来解释AspectJ的应用。

AspectJ语法整理

以下关于AspectJ的语法整理全部出自网络收集,并未一一验证,若有错误请指出。

execution

使用execution(<匹配表达式>)方法执行

匹配模式描述
* public * *(..)任何公共方法的执行
* cn.javass..IPointcutService.*()cn.javass包及所有子包下IPointcutService接口中的任何无参方法
* cn.javass..*.*(..)cn.javass包及所有子包下任何类的任何方法
* cn.javass..IPointcutService.*(*)cn.javass包及所有子包下IPointcutService接口的任何只有一个参数方法
* (!cn.javass..IPointcutService+).*(..)非“cn.javass包及所有子包下IPointcutService接口及子类型”的任何方法
* cn.javass..IPointcutService+.*()cn.javass包及所有子包下IPointcutService接口及子类型的的任何无参方法
* cn.javass..IPointcut*.test*(java.util.Date)cn.javass包及所有子包下IPointcut前缀类型的的以test开头的只有一个参数类型为java.util.Date的方法,注意该匹配是根据方法签名的参数类型进行匹配的,而不是根据执行时传入的参数类型决定的如定义方法:public void test(Object obj);即使执行时传入java.util.Date,也不会匹配的;
* cn.javass..IPointcut*.test*(..) throws IllegalArgumentException, ArrayIndexOutOfBoundsExceptioncn.javass包及所有子包下IPointcut前缀类型的的任何方法,且抛出IllegalArgumentException和ArrayIndexOutOfBoundsException异常
* (cn.javass..IPointcutService+ && java.io.Serializable+).*(..)任何实现了cn.javass包及所有子包下IPointcutService接口和java.io.Serializable接口的类型的任何方法
@java.lang.Deprecated * *(..)任何持有@java.lang.Deprecated注解的方法
@java.lang.Deprecated @cn.javass..Secure * *(..)任何持有@java.lang.Deprecated和@cn.javass…Secure注解的方法
@(java.lang.Deprecated || cn.javass..Secure) * *(..)任何持有@java.lang.Deprecated或@ cn.javass…Secure注解的方法
(@cn.javass..Secure *) *(..)任何返回值类型持有@cn.javass…Secure的方法
* (@cn.javass..Secure *).*(..)任何定义方法的类型持有@cn.javass…Secure的方法
* *(@cn.javass..Secure (*) , @cn.javass..Secure (*))任何签名带有两个参数的方法,且这个两个参数都被@ Secure标记了,如public void test(@Secure String str1, @Secure String str1);
* *((@ cn.javass..Secure *))或* *(@ cn.javass..Secure *)任何带有一个参数的方法,且该参数类型持有@cn.javass…Secure;如public void test(Model model);且Model类上持有@Secure注解
* *(@cn.javass..Secure (@cn.javass..Secure *) ,@ cn.javass..Secure (@cn.javass..Secure *))任何带有两个参数的方法,且这两个参数都被@ cn.javass…Secure标记了;且这两个参数的类型上都持有@ cn.javass…Secure;
* *(java.util.Map<cn.javass..Model, cn.javass..Model>, ..)任何带有一个java.util.Map参数的方法,且该参数类型是以< cn.javass…Model, cn.javass…Model >为泛型参数;注意只匹配第一个参数为java.util.Map,不包括子类型;如public void test(HashMap<Model, Model> map, String str);将不匹配,必须使用“* *(java.util.HashMap<cn.javass…Model,cn.javass…Model>, …)”进行匹配;而public void test(Map map, int i);也将不匹配,因为泛型参数不匹配
* *(java.util.Collection<@cn.javass..Secure *>)任何带有一个参数(类型为java.util.Collection)的方法,且该参数类型是有一个泛型参数,该泛型参数类型上持有@cn.javass…Secure注解;如public void test(Collection collection);Model类型上持有@cn.javass…Secure
* *(java.util.Set<? extends HashMap>)任何带有一个参数的方法,且传入的参数类型是有一个泛型参数,该泛型参数类型继承与HashMap;
* *(java.util.List<? super HashMap>)任何带有一个参数的方法,且传入的参数类型是有一个泛型参数,该泛型参数类型是HashMap的基类型;如public voi test(Map map);
* *(*<@cn.javass..Secure *>)任何带有一个参数的方法,且该参数类型是有一个泛型参数,该泛型参数类型上持有@cn.javass…Secure注解;

within

使用within(<匹配表达式>)方法执行

匹配模式描述
within(cn.javass..*)cn.javass包及子包下的任何方法执行
within(cn.javass..IPointcutService+)cn.javass包或所有子包下IPointcutService类型及子类型的任何方法
within(@cn.javass..Secure *)持有cn.javass…Secure注解的任何类型的任何方法必须是在目标对象上声明这个注解,在接口上声明的对它不起作用

this

使用“this(类型全限定名)”匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口方法也可以匹配;注意this中使用的表达式必须是类型全限定名,不支持通配符;

匹配模式描述
this(cn.javass.spring.chapter6.service.IPointcutService)当前AOP对象实现了 IPointcutService接口的任何方法
this(cn.javass.spring.chapter6.service.IIntroductionService)当前AOP对象实现了 IIntroductionService接口的任何方法也可能是引入接口

target

使用“target(类型全限定名)”匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;注意target中使用的表达式必须是类型全限定名,不支持通配符;

匹配模式描述
target(cn.javass.spring.chapter6.service.IPointcutService)当前目标对象(非AOP对象)实现了 IPointcutService接口的任何方法
target(cn.javass.spring.chapter6.service.IIntroductionService)当前目标对象(非AOP对象) 实现了IIntroductionService 接口的任何方法不可能是引入接口

args

使用“args(参数类型列表)”匹配当前执行的方法传入的参数为指定类型的执行方法;注意是匹配传入的参数类型,不是匹配方法签名的参数类型;参数类型列表中的参数必须是类型全限定名,通配符不支持;args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用;

匹配模式描述
args (java.io.Serializable,..)任何一个以接受“传入参数类型为 java.io.Serializable” 开头,且其后可跟任意个任意类型的参数的方法执行,args指定的参数类型是在运行时动态匹配的

@within

使用“@within(注解类型)”匹配所以持有指定注解类型内的方法;注解类型也必须是全限定类型名;

匹配模式描述
@within cn.javass.spring.chapter6.Secure)任何目标对象对应的类型持有Secure注解的类方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用

@target

使用“@target(注解类型)”匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;注解类型也必须是全限定类型名;

匹配模式描述
@target (cn.javass.spring.chapter6.Secure)任何目标对象持有Secure注解的类方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用

@args

使用“@args(注解列表)”匹配当前执行的方法传入的参数持有指定注解的执行;注解类型也必须是全限定类型名;

匹配模式描述
@args (cn.javass.spring.chapter6.Secure)任何一个只接受一个参数的方法,且方法运行时传入的参数持有注解 cn.javass.spring.chapter6.Secure;动态切入点,类似于arg指示符;

@annotation

使用“@annotation(注解类型)”匹配当前执行方法持有指定注解的方法;注解类型也必须是全限定类型名;

匹配模式描述
@annotation(cn.javass.spring.chapter6.Secure )当前执行方法上持有注解 cn.javass.spring.chapter6.Secure将被匹配

AspectJ在Android中的应用

处理Android快速点击时重复触发点击事件问题

Android中我们经常有这样的场景:点击按钮从当前页面A跳转到另一个页面B,如果点击速度特别快,会发现可能页面B被打开了多个,比如下面的代码在MainActivity页面中央,通过点击按钮打开OtherActivity:

import android.content.Intent;
import android.os.Bundle;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this, OtherActivity.class));
            }
        });
    }
}

如果你在真机上测试,会发现快速点击按钮时,可能打开OtherActivity两次甚至三次,为了处理这种快速点击导致的问题,一般会这么做:
编写一个工具类,提供一个方法用于判断当前点击时间和上次点击时间的差,如果时间差比较短(比如500毫秒),认为是快速点击,则不处理这次点击事件,比如下面的代码:

public class ClickUtil {
    
    private static long lastClickTime = 0L;
    
    // 是否是快速点击
    public static boolean isFastClick() {
        long curTime = System.currentTimeMillis();
        if (curTime - lastClickTime >= 500L) {
            lastClickTime = curTime;
            return true;
        }
        return false;
    }
    
}

在点击事件相关的逻辑中调用上面的代码:

if (!ClickUtil.isFastClick()) {
    startActivity(new Intent(MainActivity.this, OtherActivity.class));
}

这种方式可以防止快速点击,但是有一些问题:

  1. 代码具有侵入性,需要在原有的代码逻辑上做修改
  2. 比较复杂,如果有很多地方都需要处理点击事件过快,则需要加很多代码

我们可以使用AspectJ面向切面编程来处理这种问题,比如编写如下切入点代码:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class MethodAspect {

    // 点击事件触发的时间间隔为600毫秒,即600毫秒内只允许一次点击
    private static final long CLICK_INTERVAL = 600L;
    // 上次点击的时间
    private static long lastClickTime = 0L;

    // 是否允许点击事件
    private boolean isClickEnabled() {
        long curTime = System.currentTimeMillis();
        if (curTime - lastClickTime > CLICK_INTERVAL) {
            lastClickTime = curTime;
            return true;
        }
        return false;
    }
	
	// 该方法会匹配android.view包及所有子包下的OnClickListener接口中的onClick方法,且方法参数为android.view.View
	// 注意使用的@Around而不是@Before,且方法参数为ProceedingJoinPoint
    @Around("execution(* android.view..OnClickListener.onClick(android.view.View))")
    public void clickableDetect(ProceedingJoinPoint joinPoint) {
        if (isClickEnabled()) {
        	// 如果点击事件被允许,则执行原来的逻辑
            try {
                joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
    }

}

在MainActivity中不用做任何更改,上面的切入点代码会自动匹配按钮的点击事件,并修改编译后的字节码,在原有的点击逻辑周围加入点击是否被允许的逻辑判断,我们可以在app/build/intermediates/javac/debug/classed目录下查看MainActivity.class反编译后的代码,代码如下:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    public MainActivity() {
    }

    protected void onCreate(Bundle savedInstanceState) {
        MethodAspect.aspectOf().beforeOnCreate();
        super.onCreate(savedInstanceState);
        this.setContentView(2131427356);
        this.findViewById(2131230807).setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                JoinPoint var3 = Factory.makeJP(ajc$tjp_0, this, this, v);
                onClick_aroundBody1$advice(this, v, var3, MethodAspect.aspectOf(), (ProceedingJoinPoint)var3);
            }

            static {
                ajc$preClinit();
            }
        });
    }
}

可以看到onClick方法体中已经多了一些被AspectJ处理过的逻辑,在真机上测试快速点击,已经不会出现多次触发点击事件的问题了。

这种使用AspectJ AOP编程处理快速点击多次触发的问题较上面直接编码的优势在于:代码无侵入性,不需要修改原来的点击事件逻辑,通过AspectJ框架可自动在点击事件的逻辑前后插入代码,完成点击是否过快的逻辑判断,但是这种方式完美吗?

并不完美!

我们知道在Android中处理点击事件的方式有很多种:

  • 可以直接给某个View设置setOnClickListener然后传入匿名的View.OnClickListener
  • 可以在类上实现View.OnClickListener接口然后设置某个View的点击事件为this
  • 可以在类中定义一个成员变量listener,其类型为View.OnClickListener,然后指定某个View的点击事件为listener

还有很多其他方式能为View添加点击事件,另外,不是所有的View的点击事件内部都是做的页面跳转,某些点击事件可能就算重复触发多次也没有什么问题,如果统一用上面AspectJ匹配onClick方法来处理,可能会有误伤;另外,如果某个项目中有很多onClick相关的点击事件方法,使用AspectJ去匹配并修改这些方法,将会给编译项目增加很大负担(编译时间会变长),所以这种方式并不完美,下面用另一种基于AspectJ的方法来更优雅的处理点击事件重复触发多次的问题,用到了自定义注解。

首先我们定义如下注解:

// 作用在方法上
@Target(ElementType.METHOD)
// 注解保留到字节码阶段
@Retention(RetentionPolicy.CLASS)
public @interface ClickOnce {
}

我们要实现的功能是,使用@ClickOnce注解的方法,在600毫秒内只触发一次;

然后编写切入点方法如下代码:

@Aspect
public class MethodAspect {

    // 点击事件触发的时间间隔为600毫秒,即600毫秒内只允许一次点击
    private static final long CLICK_INTERVAL = 600L;
    // 上次点击的时间
    private static long lastClickTime = 0L;

    // 是否允许点击事件
    private boolean isClickEnabled() {
        long curTime = System.currentTimeMillis();
        if (curTime - lastClickTime > CLICK_INTERVAL) {
            lastClickTime = curTime;
            return true;
        }
        return false;
    }

	// 匹配的是使用@ClickOnce注解的任意方法
    @Around("execution(@ClickOnce * *(..))")
    public void clickOnce(ProceedingJoinPoint joinPoint) {
        if (isClickEnabled()) {
            try {
                joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
    }

}

下面测试一下@ClickOnce注解能否正常工作,在MainActivity中增加两个按钮,分别用两种不同的方式为这两个按钮添加点击事件,点击还是跳转到OtherActivity:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

		// 直接设置点击事件处理器
        findViewById(R.id.btn2).setOnClickListener(new View.OnClickListener() {
            @Override
            @ClickOnce
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this, OtherActivity.class));
            }
        });

		// 使用类上实现View.OnClickListener接口这种方式为按钮绑定点击事件处理器
        findViewById(R.id.btn3).setOnClickListener(this);
    }

    @ClickOnce
    private void toOtherActivity() {
        startActivity(new Intent(MainActivity.this, OtherActivity.class));
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn3) {
            toOtherActivity();
        }
    }
}

然后我们编译项目,再次查看生成的MainActivity.class文件反编译后的代码:

public class MainActivity extends AppCompatActivity implements OnClickListener {
    public MainActivity() {
    }

    protected void onCreate(Bundle savedInstanceState) {
        MethodAspect.aspectOf().beforeOnCreate();
        super.onCreate(savedInstanceState);
        this.setContentView(2131427356);
        this.findViewById(2131230808).setOnClickListener(new OnClickListener() {
            @ClickOnce
            public void onClick(View v) {
                JoinPoint var3 = Factory.makeJP(ajc$tjp_0, this, this, v);
                onClick_aroundBody1$advice(this, v, var3, MethodAspect.aspectOf(), (ProceedingJoinPoint)var3);
            }

            static {
                ajc$preClinit();
            }
        });
        this.findViewById(2131230809).setOnClickListener(this);
    }

    @ClickOnce
    private void toOtherActivity() {
        JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
        toOtherActivity_aroundBody1$advice(this, var1, MethodAspect.aspectOf(), (ProceedingJoinPoint)var1);
    }

    public void onClick(View v) {
        if (v.getId() == 2131230809) {
            this.toOtherActivity();
        }

    }

    static {
        ajc$preClinit();
    }
}

可以看到被@ClickOnce注解的方法体内都被AspectJ插入的新的代码,我们将项目运行到真机上,测试可以看到快速点击重复触发点击事件的问题没有了,证明以上代码能正常工作。

这种使用自定义注解的方式,相较直接用AspectJ匹配onClick方法的方式,又更进了一步,不仅不会侵入原有代码逻辑,而且更灵活:需要防止重复点击时就使用自定义注解,否则就不用。

总结

AspectJ是一个Java AOP框架,它可以作用于Java编译后的字节码文件,从而实现某些功能,跟面向对象OOP编程相比,AOP在处理某些切面问题时更灵活且优雅,但是AspectJ的使用一定要小心,针对切面的匹配规则一定要详细测试,不当的匹配规则可能会导致代码编译时间变长,且可能处理了我们并不需要处理的逻辑从而导致某些错误。

参考

https://blog.csdn.net/sunlihuo/article/details/52701548

源码

本篇的源码可以在这里下载:https://github.com/yubo725/android-aspectj-demo/tree/v0.2

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

yubo_725

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值