欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android面向切面(AOP)编程实战

程序员文章站 2022-04-07 19:32:54
AOP:面向切面编程(Aspect-Oriented Programming)。如果说,OOP如果是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理...

AOP:面向切面编程(Aspect-Oriented Programming)。如果说,OOP如果是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。 Aspect介绍篇:Android中的AOP编程 这里通过几个小例子,讲解在Android开发中,如何运用AOP的方式,进行全局切片管理,达到简洁优雅,一劳永逸的效果。

1、SingleClickAspect,防止View被连续点击出发多次事件

在使用aop之前,可以这样写了单独写个Click类(不优雅)或者RxBinding(不简洁):

 RxView.clicks(mButton)
                .throttleFirst(1, TimeUnit.SECONDS)
                .subscribe(new Action1() {

                    @Override
                    public void call(Void v) {
                        dosomething();
                    }
                });

现在,只需要一个注解,就可以轻松解决一切问题:

@Aspect
public class SingleClickAspect {
    static int TIME_TAG = R.id.click_time;
    public static final int MIN_CLICK_DELAY_TIME = 600;//间隔时间600ms

    @Pointcut("execution(@com.app.annotation.aspect.SingleClick * *(..))")//根据SingleClick注解找到方法切入点
    public void methodAnnotated() {
    }

    @Around("methodAnnotated()")//在连接点进行方法替换
    public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        View view = null;
        for (Object arg : joinPoint.getArgs())
            if (arg instanceof View) view = (View) arg;
        if (view != null) {
            Object tag = view.getTag(TIME_TAG);
            long lastClickTime = ((tag != null) ? (long) tag : 0);
            LogUtils.showLog("SingleClickAspect", "lastClickTime:" + lastClickTime);
            long currentTime = Calendar.getInstance().getTimeInMillis();
            if (currentTime - lastClickTime > MIN_CLICK_DELAY_TIME) {//过滤掉600毫秒内的连续点击
                view.setTag(TIME_TAG, currentTime);
                LogUtils.showLog("SingleClickAspect", "currentTime:" + currentTime);
                joinPoint.proceed();//执行原方法
            }
        }
    }
}

使用方法:标注在onClick上

   @SingleClick
    public void onClick(View view) {
        String comment = mViewBinding.btComment.getText().toString();
        if (TextUtils.isEmpty(comment))
            Snackbar.make(mViewBinding.fab, "评论不能为空!", Snackbar.LENGTH_LONG).show();
        else mPresenter.createComment(comment, mArticle, SpUtil.getUser());
    }

或者任何参数内有view可以做为参照系(view可以不是onClick的view,仅仅作为时间tag依附对象作为参照)的方法上,例如TRouter的页面跳转,防止连续快速点击重复跳页现象:

public class RouterHelper {
    
    @SingleClick // 防止连续点击
    public static void go(String actionName, HashMap data, View view) {
        TRouter.go(actionName, data, view);
    }
}

2、CheckLoginAspect 拦截未登录用户的权限

不使用aop的情况,需要在每个方法体内判断用户登录状态,然后处理,现在,只需要一个注解轻松解决:


/**
 * Created by baixiaokang
 * 通过CheckLogin注解检查用户是否登陆注解,通过aop切片的方式在编译期间织入源代码中
 * 功能:检查用户是否登陆,未登录则提示登录,不会执行下面的逻辑
 */
@Aspect
public class CheckLoginAspect {

    @Pointcut("execution(@com.app.annotation.aspect.CheckLogin * *(..))")//方法切入点
    public void methodAnnotated() {
    }

    @Around("methodAnnotated()")//在连接点进行方法替换
    public void aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        if (null == SpUtil.getUser()) {
            Snackbar.make(App.getAppContext().getCurActivity().getWindow().getDecorView(), "请先登录!", Snackbar.LENGTH_LONG)
                    .setAction("登录", new View.OnClickListener() {
                        @Override
                        public void onClick(View view) {
                            TRouter.go(C.LOGIN);
                        }
                    }).show();
            return;
        }
        joinPoint.proceed();//执行原方法
    }
}

使用方法:

public class AdvisePresenter extends AdviseContract.Presenter {

    @CheckLogin
    public void createMessage(String msg) {
        _User user = SpUtil.getUser();
        ApiFactory.createMessage(
                new Message(ApiUtil.getPointer(
                        new _User(C.ADMIN_ID)), msg,
                        ApiUtil.getPointer(user),
                        user.objectId))
                .subscribe(
                        res -> mView.sendSuc(),
                        e -> mView.showMsg("消息发送失败!"));
    }

    @CheckLogin
    public void initAdapterPresenter(AdapterPresenter mAdapterPresenter) {
        mAdapterPresenter
                .setRepository(ApiFactory::getMessageList)
                .setParam(C.INCLUDE, C.CREATER)
                .setParam(C.UID, SpUtil.getUser().objectId)
                .fetch();
    }
}

从此只需要专注主要逻辑即可。

3、MemoryCacheAspect内存缓存切片

根据参数key缓存方法返回值,使我们纯净的Presenter(无参构造和无内部状态)达到全局缓存的单例复用效果,同样适用于其他需要缓存结果的方法:

/**
 * Created by baixiaokang on 16/10/24.
 * 根据MemoryCache注解自动添加缓存代理代码,通过aop切片的方式在编译期间织入源代码中
 * 功能:缓存某方法的返回值,下次执行该方法时,直接从缓存里获取。
 */
@Aspect
public class MemoryCacheAspect {

    @Pointcut("execution(@com.app.annotation.aspect.MemoryCache * *(..))")//方法切入点
    public void methodAnnotated() {
    }

    @Around("methodAnnotated()")//在连接点进行方法替换
    public Object aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        String methodName = methodSignature.getName();
        MemoryCacheManager mMemoryCacheManager = MemoryCacheManager.getInstance();
        StringBuilder keyBuilder = new StringBuilder();
        keyBuilder.append(methodName);
        for (Object obj : joinPoint.getArgs()) {
            if (obj instanceof String) keyBuilder.append((String) obj);
            else if (obj instanceof Class) keyBuilder.append(((Class) obj).getSimpleName());
        }
        String key = keyBuilder.toString();
        Object result = mMemoryCacheManager.get(key);//key规则 : 方法名+参数1+参数2+...
        LogUtils.showLog("MemoryCache", "key:" + key + "--->" + (result != null ? "not null" : "null"));
        if (result != null) return result;//缓存已有,直接返回
        result = joinPoint.proceed();//执行原方法
        if (result instanceof List && result != null && ((List) result).size() > 0 //列表不为空
                || result instanceof String && !TextUtils.isEmpty((String) result)//字符不为空
                || result instanceof Object && result != null)//对象不为空
            mMemoryCacheManager.add(key, result);//存入缓存
        LogUtils.showLog("MemoryCache", "key:" + key + "--->" + "save");
        return result;
    }
}

看看Apt生成的Factory:

/**
 * @ 实例化工厂 此类由apt自动生成 */
public final class InstanceFactory {
  /**
   * @此方法由apt自动生成 */
  @MemoryCache
  public static Object create(Class mClass) throws IllegalAccessException, InstantiationException {
     switch (mClass.getSimpleName()) {
      case "AdvisePresenter": return  new AdvisePresenter();
      case "ArticlePresenter": return  new ArticlePresenter();
      case "HomePresenter": return  new HomePresenter();
      case "LoginPresenter": return  new LoginPresenter();
      case "UserPresenter": return  new UserPresenter();
      default: return mClass.newInstance();
    }
  }
}

从此Presenter就是全局单例的可复用状态。

4、TimeLogAspect 自动打印方法的耗时

经常遇到需要log一个耗时操作究竟执行了多长时间,无aop时,需要每个方法体内添加代码,现在,只需要一个注解就可以一劳永逸:

/**
 * 根据注解TimeLog自动添加打印方法耗代码,通过aop切片的方式在编译期间织入源代码中
 * 功能:自动打印方法的耗时
 */
@Aspect
public class TimeLogAspect {

    @Pointcut("execution(@com.app.annotation.aspect.TimeLog * *(..))")//方法切入点
    public void methodAnnotated() {
    }

    @Pointcut("execution(@com.app.annotation.aspect.TimeLog *.new(..))")//构造器切入点
    public void constructorAnnotated() {
    }

    @Around("methodAnnotated() || constructorAnnotated()")//在连接点进行方法替换
    public Object aroundJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        LogUtils.showLog("TimeLog getDeclaringClass", methodSignature.getMethod().getDeclaringClass().getCanonicalName());
        String className = methodSignature.getDeclaringType().getSimpleName();
        String methodName = methodSignature.getName();
        long startTime = System.nanoTime();
        Object result = joinPoint.proceed();//执行原方法
        StringBuilder keyBuilder = new StringBuilder();
        keyBuilder.append(methodName + ":");
        for (Object obj : joinPoint.getArgs()) {
            if (obj instanceof String) keyBuilder.append((String) obj);
            else if (obj instanceof Class) keyBuilder.append(((Class) obj).getSimpleName());
        }
        String key = keyBuilder.toString();
        LogUtils.showLog("TimeLog", (className + "." + key + joinPoint.getArgs().toString() + " --->:" + "[" + (TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) + "ms]"));// 打印时间差
        return result;
    }
}

使用方法:

  @TimeLog
    public void onCreate() {
        super.onCreate();
        mApp = this;
        SpUtil.init(this);
        store = new Stack<>();
        registerActivityLifecycleCallbacks(new SwitchBackgroundCallbacks());
    }

从此方法耗时打印一个注解搞定!

5、SysPermissionAspect运行时权限申请

/**
 * 申请系统权限切片,根据注解值申请所需运行权限
 */
@Aspect
public class SysPermissionAspect {

    @Around("execution(@com.app.annotation.aspect.Permission * *(..)) && @annotation(permission)")
    public void aroundJoinPoint(ProceedingJoinPoint joinPoint, Permission permission) throws Throwable {
        AppCompatActivity ac = (AppCompatActivity) App.getAppContext().getCurActivity();
        new AlertDialog.Builder(ac)
                .setTitle("提示")
                .setMessage("为了应用可以正常使用,请您点击确认申请权限。")
                .setNegativeButton("取消", null)
                .setPositiveButton("允许", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        MPermissionUtils.requestPermissionsResult(ac, 1, permission.value()
                                , new MPermissionUtils.OnPermissionListener() {
                                    @Override
                                    public void onPermissionGranted() {
                                        try {
                                            joinPoint.proceed();//获得权限,执行原方法
                                        } catch (Throwable e) {
                                            e.printStackTrace();
                                        }
                                    }

                                    @Override
                                    public void onPermissionDenied() {
                                        MPermissionUtils.showTipsDialog(ac);
                                    }
                                });
                    }
                })
                .create()
                .show();
    }
}

使用方法:

   @Permission(Manifest.permission.CAMERA)
    public void takePhoto() {
        startActivityForResult(
                new Intent(MediaStore.ACTION_IMAGE_CAPTURE)
                        .putExtra(MediaStore.EXTRA_OUTPUT,
                                Uri.fromFile(new File(getExternalCacheDir()+ "user_photo.png"))),
                C.IMAGE_REQUEST_CODE);
    }

动态权限申请一步搞定。

除了这些简单的示例,AOP还可以实现动态权限申请和其他用户权限管理,包括功能性切片和逻辑性切片,使日常开发更加简洁优雅,只需要关注重点业务逻辑,把其他的小事,都交给切片来自动处理吧。

更多AOP的实际应用,请关注项目T-MVP

或者加群来搞基:

QQ群:AndroidMVP 555343041

更新日志:

2017/1/31:AOP新增SysPermissionAspect支持动态申请系统权限切片,轻松适配6.0+

2017/1/27:AOP新增DbRealmAspect支持Realm数据库,数据库突破你想像的简单(年夜特供)

2017/1/8: 使用Apt封装Retrofit生成ApiFactory替换掉所有的Repository,狂删代码

2017/1/7: 使用DataBinding替换掉所有的ButterKnife,狂删代码

2017/1/6: 使用DataBinding替换掉所有的ViewHolder,狂删代码,从此迈向新时代

2016/12/30:使用Apt生成全局路由TRouter,更优雅的页面跳转,支持传递参数和共享view转场动画

2016/12/29:去掉BaseMultiVH新增VHClassSelector支持更完美的多ViewHolder

2016/12/28:使用Apt生成全局的ApiFactory替代所有的Model

2016/12/27:增加了BaseMultiVH扩展支持多类型的ViewHolder

2016/12/26:抽离CoreAdapterPresenter优化TRecyclerView

安卓AOP实战:面向切片编程 Android实用技巧之:用好泛型,少写代码 安卓AOP实战:APT打造极简路由

全局路由TRouter,更优雅的页面跳转

安卓AOP实战:Javassist强撸EventBus

加入OkBus,实现注解传递事件

安卓AOP三剑客:APT,AspectJ,Javassist

1、去掉所有反射>2、新增apt初始化工厂,替换掉了dagger2。>3、新增aop切片,处理缓存和日志