Android 面向切面编程

220 阅读5分钟

一、AOP即面向切面编程

AOP 是 [Aspect] Oriented Programming 的缩写,译为面向切面编程。它提倡的是针对同一类问题的统一处理,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

二、AOP使用场景

  1. 全局日志打印。
  2. 全局判断登录状态
  3. 防重复点击事件
  4. 动态权限申请
  5. 缓存
  6. ...

三、AspectJ基本概念

  • Aspect 切面:切面是切入点和通知的集合。
  • PointCut 切入点:切入点是指那些通过使用一些特定的表达式过滤出来的想要切入Advice的连接点。
  • Advice 通知:通知是向切点中注入的代码实现方法。
  • Joint Point 连接点:所有的目标方法都是连接点.
  • Weaving 编织:主要是在编译期使用AJC将切面的代码注入到目标中, 并生成出代码混合过的.class的过程.

四、AspectJ 常用注解介绍

  • @Aspect:声明切面,标记类
  • @Pointcut(切点表达式):定义切点,标记方法
  • @Before(切点表达式):前置通知,切点之前执行
  • @Around(切点表达式):环绕通知,切点前后执行
  • @After(切点表达式):后置通知,切点之后执行
  • @AfterReturning(切点表达式):返回通知,切点方法返回结果之后执行
  • @AfterThrowing(切点表达式):异常通知,切点抛出异常时执行

五、AspectJ 使用

如何引入
1、在项目的 build.gradle 中引入 aspectj 的包

buildscript {
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.14'
        classpath 'org.aspectj:aspectjweaver:1.8.14'
    }
}

2、app/build.gradle引入aspectj插件

dependencies { 
implementation fileTree(dir: 'libs', include: ['*.jar']) 
implementation 'org.aspectj:aspectjrt:1.8.14' 
}

这儿列举一个防止重复点击的例子

1、创建切面

@Aspect
public class OnClickAspect {
 ...
}

2、定义切点方法

//普通view的点击事件
private static final String POINTCUT_METHOD =
        "execution(* android.view.View.OnClickListener.onClick(..))";
//recyclerview的点击事件
private static final String POINTCUT_METHODITEM =
        "execution(* android.widget.AdapterView.OnItemClickListener.onItemClick(..))";
private static final String POINTCUT_ANNOTATION =
        "execution(@com.cdrcbperson.mobilebank.base.aop_repeat_click.SingleClick * *(..))";
//通过butterknife实现的点击事件
private static final String POINTCUT_BUTTER_KNIFE =
        "execution(@butterknife.OnClick * *(..))";
// 根据业务场景自行添加.......

//定义切点方法
@Pointcut(POINTCUT_METHOD)
public void methodPointcut() {

}

@Pointcut(POINTCUT_METHODITEM)
public void methodItemPointcut() {

}

@Pointcut(POINTCUT_ANNOTATION)
public void annotationPointcut() {

}

@Pointcut(POINTCUT_BUTTER_KNIFE)
public void butterKnifePointcut() {

}

3、实现切面

    @Around("methodPointcut() || annotationPointcut() || butterKnifePointcut() || methodItemPointcut()")
    public void aroundJoinPoint(final ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            //计算点击间隔,没有注解默认1000,有注解按注解参数来,注解参数为空默认1000;
            int interval = 1000;
            //获取被点击的view对象
            Object[] args = joinPoint.getArgs();
            View view = findViewInMethodArgs(args);
            if (view != null) {
                int id = view.getId();
                if (mLastClickViewId != id) {
                    mLastClickViewId = id;
                    mLastClickTime = System.currentTimeMillis();
                    joinPoint.proceed();
                    return;
                }
                mLastClickViewId = id;
                if (canClick(interval)) {
                    mLastClickTime = System.currentTimeMillis();
                    joinPoint.proceed();
                    return;
                }
            } else {
                joinPoint.proceed();
                return;
            }

            //检测间隔时间是否达到预设时间并且线程空闲
            if (canClick(interval)) {
                mLastClickTime = System.currentTimeMillis();
                joinPoint.proceed();
            }
        } catch (Exception e) {
            //出现异常不拦截点击事件
            joinPoint.proceed();
        }
    }

    public View findViewInMethodArgs(Object[] args) {
        if (args == null || args.length == 0) {
            return null;
        }
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof View) {
                View view = (View) args[i];
                if (view.getId() != View.NO_ID) {
                    return view;
                }
            }
        }
        return null;
    }

    public boolean canClick(int interval) {
        long l = Math.abs(System.currentTimeMillis() - mLastClickTime);
        if (l > interval) {
            mLastClickTime = System.currentTimeMillis();
            return true;
        }
        return false;
    }

3、完整代码

@Aspect
public class OnClickAspect {
    private static long mLastClickTime;

    private static int mLastClickViewId = -1;

    private static final String POINTCUT_METHOD =
            "execution(* android.view.View.OnClickListener.onClick(..))";
    private static final String POINTCUT_METHODITEM =
            "execution(* android.widget.AdapterView.OnItemClickListener.onItemClick(..))";
    private static final String POINTCUT_ANNOTATION =
            "execution(@com.cdrcbperson.mobilebank.base.aop_repeat_click.SingleClick * *(..))";
    private static final String POINTCUT_BUTTER_KNIFE =
            "execution(@butterknife.OnClick * *(..))";

    @Pointcut(POINTCUT_METHOD)
    public void methodPointcut() {

    }

    @Pointcut(POINTCUT_METHODITEM)
    public void methodItemPointcut() {

    }

    @Pointcut(POINTCUT_ANNOTATION)
    public void annotationPointcut() {

    }

    @Pointcut(POINTCUT_BUTTER_KNIFE)
    public void butterKnifePointcut() {

    }

    //环绕切面
    @Around("methodPointcut() || annotationPointcut() || butterKnifePointcut() || methodItemPointcut()")
    public void aroundJoinPoint(final ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            //计算点击间隔,没有注解默认1000,有注解按注解参数来,注解参数为空默认1000;
            int interval = 1000
            //获取被点击的view对象
            Object[] args = joinPoint.getArgs();
            View view = findViewInMethodArgs(args);
            if (view != null) {
                int id = view.getId();
                if (mLastClickViewId != id) {
                    mLastClickViewId = id;
                    mLastClickTime = System.currentTimeMillis();
                    joinPoint.proceed();
                    return;
                }
                mLastClickViewId = id;
                if (canClick(interval)) {
                    mLastClickTime = System.currentTimeMillis();
                    joinPoint.proceed();
                    return;
                }
            } else {
                joinPoint.proceed();
                return;
            }

            //检测间隔时间是否达到预设时间并且线程空闲
            if (canClick(interval)) {
                mLastClickTime = System.currentTimeMillis();
                joinPoint.proceed();
            }
        } catch (Exception e) {
            //出现异常不拦截点击事件
            joinPoint.proceed();
        }
    }

    public View findViewInMethodArgs(Object[] args) {
        if (args == null || args.length == 0) {
            return null;
        }
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof View) {
                View view = (View) args[i];
                if (view.getId() != View.NO_ID) {
                    return view;
                }
            }
        }
        return null;
    }

    public boolean canClick(int interval) {
        long l = Math.abs(System.currentTimeMillis() - mLastClickTime);
        if (l > interval) {
            mLastClickTime = System.currentTimeMillis();
            return true;
        }
        return false;
    }
}

六、优化

  • 针对某些特定的场景不需要防止重复点击,比如说连续点赞,我们需要单独处理一下

1、定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnSingleClick {
    //防重复点击间隔
    int value() default 1000;
    //根据id实现不需要防止重复点击事件  
    int[] except() default {};
}

2、实现方式

@Aspect
public class OnClickAspect {
    private static long mLastClickTime;

    private static int mLastClickViewId = -1;

    private static final String POINTCUT_METHOD =
            "execution(* android.view.View.OnClickListener.onClick(..))";
    private static final String POINTCUT_METHODITEM =
            "execution(* android.widget.AdapterView.OnItemClickListener.onItemClick(..))";
    private static final String POINTCUT_ANNOTATION =
            "execution(@com.cdrcbperson.mobilebank.base.aop_repeat_click.SingleClick * *(..))";
    private static final String POINTCUT_BUTTER_KNIFE =
            "execution(@butterknife.OnClick * *(..))";

    @Pointcut(POINTCUT_METHOD)
    public void methodPointcut() {

    }

    @Pointcut(POINTCUT_METHODITEM)
    public void methodItemPointcut() {

    }

    @Pointcut(POINTCUT_ANNOTATION)
    public void annotationPointcut() {

    }

    @Pointcut(POINTCUT_BUTTER_KNIFE)
    public void butterKnifePointcut() {

    }

    @Around("methodPointcut() || annotationPointcut() || butterKnifePointcut() || methodItemPointcut()")
    public void aroundJoinPoint(final ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            Method method = signature.getMethod();
            //检查方法是否有注解
            boolean hasAnnotation = method != null && method.isAnnotationPresent(OnSingleClick.class);
            //计算点击间隔,没有注解默认1000,有注解按注解参数来,注解参数为空默认1000;
            int interval = 1000;
            if (hasAnnotation) {
                SingleClick annotation = method.getAnnotation(OnSingleClick.class);
                interval = annotation.value();
            }
            //获取被点击的view对象
            Object[] args = joinPoint.getArgs();
            View view = findViewInMethodArgs(args);
            if (view != null) {
                int id = view.getId();
                if (mLastClickViewId != id) {
                    mLastClickViewId = id;
                    mLastClickTime = System.currentTimeMillis();
                    joinPoint.proceed();
                    return;
                }
                mLastClickViewId = id;
                //注解排除某个控件不防止双击
                if (hasAnnotation) {
                    SingleClick annotation = method.getAnnotation(SingleClick.class);
                    //按id值排除不防止双击的按钮点击
                    int[] except = annotation.except();
                    for (int i : except) {
                        if (i == id) {
                            mLastClickTime = System.currentTimeMillis();
                            joinPoint.proceed();
                            return;
                        }
                    }
                }
                if (canClick(interval)) {
                    mLastClickTime = System.currentTimeMillis();
                    joinPoint.proceed();
                    return;
                }
            } else {
                joinPoint.proceed();
                return;
            }

            //检测间隔时间是否达到预设时间并且线程空闲
            if (canClick(interval)) {
                mLastClickTime = System.currentTimeMillis();
                joinPoint.proceed();
            }
        } catch (Exception e) {
            //出现异常不拦截点击事件
            joinPoint.proceed();
        }
    }

    public View findViewInMethodArgs(Object[] args) {
        if (args == null || args.length == 0) {
            return null;
        }
        for (int i = 0; i < args.length; i++) {
            if (args[i] instanceof View) {
                View view = (View) args[i];
                if (view.getId() != View.NO_ID) {
                    return view;
                }
            }
        }
        return null;
    }

    public boolean canClick(int interval) {
        long l = Math.abs(System.currentTimeMillis() - mLastClickTime);
        if (l > interval) {
            mLastClickTime = System.currentTimeMillis();
            return true;
        }
        return false;
    }
}

3、使用

view.setOnClickListener(new View.OnClickListener() {
   //两种方式实现1、设置value =0 间隔为0  2、根据id实现
   @OnSingleClick(value = 0)
   @Override
   public void onClick(View view) {
       
   }
});