Android AOP编程学习与实践

AOP背景:

把我们某个方面的功能提出来与一批对象进行隔离,这样与一批对象之间的耦合度就降低了,就只需要对某个功能进行编程。例如android中的登陆权限问题,只需要在特定的方法加入我们的登陆切点,在不改变业务逻辑的情况下可以变更我们判断登录 的业务逻辑,这样就达到了松耦合的目的,在编译成.class时注入.

如果说,OOP是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理.

Android AOP就是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,提高开发效率

 

什么是面向切面编程?

在运行期间,动态的将代码块切入到特定的类、代码块的编程思想就是面向切面编程

特点:1、针对同一类问题统一处理

            2、无侵入添加代码

AOP用途

AOP可用于日志埋点、性能监控、动态权限控制、甚至是代码调试等等

 

AOP应用之Java Aspect 

基础知识

AspectJ

是一个面向切面编程的框架,他扩展了java语言所以他有一个专门的编译器用来生成遵循java字节码规范的Class文件

Aspect:切面      [切入指定类、指定方法的代码块称为切面]

Pointcut:切点    [切入到那些类、哪些方法则称为切点]

Around:处理

joinPoint(连接点):[aspectJ中的ProceedingJoinPoint]和方法有关的前前后后都是连接点

Advice: 一种hook,要插入代码的位置

              Before pointcut之前;After pointCut之后; Around pointCut之前之后执行

exccution() 切入点函数

注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

 定义一个注解都要包含 @Target @Retention

@Target  标识运用在哪里(是方法/类/....)

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

@Retention 注解保留策略

  • Source  注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;在class字节码文件中不包含注解
  •  Class   默认的保留策略,注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期,但运行时无法获得
  • Runtime  注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;在运行时可以通过反射获取到
public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     * 源码注解   
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     * 编译时注解
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     * 运行时注解   
     */
    RUNTIME
}

 

案例

下面将以一个简单的案例来分析登录的注解情况。

实现VIP登录校验,并弹出toast

gradle引入AspectJ

下面的依赖是我在2020/07/23时更新的依赖,之前老版本的AspectJ貌似并不适用,报一些奇怪的错误

app.gradle中
apply plugin: 'com.android.application'
apply plugin: 'android-aspectjx'

implementation 'org.aspectj:aspectjrt:1.9.5'


project.gradle中
 dependencies {
   classpath 'com.android.tools.build:gradle:3.0.1'
   // 需要升级aspectj版本,但是不适配高版本gradle
   classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.8'
}

定义注解:

/**
 * @author crazyZhangxl on 2019/1/28.
 * Describe: 需要登陆验证的切点 注意在interface前加入@
 */

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginTrace {
}

定义切点切面以及特定的处理:

/**
 * @author crazyZhangxl on 2019/1/28.
 * Describe: 切面
 */
@Aspect
public class LoginAspect {

    /**
     * 对含有某个方法的特定注解打上切点
     * 匹配注解为"@com.example.aopproject.login_demo.LoginTrace",返回值为任意类型,任意包名下的任意方法  
     */
    @Pointcut("execution(@com.example.aopproject.login_demo.LoginTrace * *(..))")
    public void pointCutLogin(){

    }

    /**
     * 处理 特定的打上切点的方法
     * @param proceedingJoinPoint
     * @throws Throwable
     */
    @Around("pointCutLogin()")
    public void aroundLogin(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        if (UserCache.getInstance().isLogin()){
            proceedingJoinPoint.proceed();
        }else {
            Toast.makeText(MyApp.getmContext(), "请先进行登陆!", Toast.LENGTH_SHORT).show();
        }

    }
}

 方法业务逻辑 

    @LoginTrace()
    private void methodTwo(){
        Toast.makeText(this, "执行VIP动作", Toast.LENGTH_SHORT).show();
    }

具体的运行效果:

在登录的情况下进行VIP浏览操作成功,而若在未登录的情况下操作时那么会进行弹框提示。

优化:

上一个案例只是最简单的加了aop方法运行时的处理,下面将进行拓展,让外部去实现具体需要在未登录的情况下去执行哪些处理

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginTrace {
    int type();
}
    /**
     * 处理 特定的打上切点的方法
     * @param proceedingJoinPoint
     * @throws Throwable
     */
    @Around("pointCutLogin()")
    public void aroundLogin(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        if (UserCache.getInstance().isLogin()){
            proceedingJoinPoint.proceed();
        }else {
            if (proceedingJoinPoint.getThis() instanceof Context){
                // 获得注解参数
                MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
                LoginTrace annotation = signature.getMethod().getAnnotation(LoginTrace.class);
                int type = annotation.type();

                dealWithType(type,(Context) proceedingJoinPoint.getThis());
            }
        }
    }

    /**
     * 在这里处理是弹出dialog呢还是跳转界面呢 等等
     * @param type
     * @param context
     */
    private void dealWithType(int type,Context context){
        switch (type){
            case 0:
                Intent intent = new Intent(context,LoginActivity.class);
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                context.startActivity(intent);
                break;
                default:
                    Toast.makeText( context,"请先进行登陆!", Toast.LENGTH_SHORT).show();
                    break;
        }
    }

AOP 应用之Lancet

Lancet官方文档

Lancet是一个轻量级Android AOP框架  饿了么开源项目

  • 编译速度快,并支持增量编译
  • 简介的API,几行Java代码完成注入需求
  • 没有任何多余代码插入apk
  • 支持用于SDK,可以在SDK编写注入代码来修改依赖SDK的App
  •  

@Proxy 将使用新的方法替换代码里存在的原有的目标方法。

@Insert 将新代码插入到目标方法原有代码前后,常用于操作App与library的类;[常用]

  • @TargetClass
    通过类查找
  1. value => 需要查找的类名(可写具体类或者父类)
  2. Scope.SELF仅代表匹配value指定的目标类
  3. Scope.DIRECT代表匹配value指定类的直接子类
  4. Scope.ALL代表匹配value指定类的所有子类
  5. Scope.LEAF代表匹配value指定类的最终子类。众所周知java是单继承,所以继承关系是树形结构,这里代表了指定类为顶点的继承树的所有叶子节点。

@ClassOf

用于形参类型为对象场景
可以使用ClassOf注解来替代对类的直接import

/**
 * Created by apple on 2020/8/7.
 * description: 启动时间hook + 单一方法统计时间
 */
public class LaunchHook {
    public static TimeRecord timeRecord;

    static {
        timeRecord = new TimeRecord();
    }

    @TargetClass(value = "androidx.appcompat.app.AppCompatActivity",scope = Scope.ALL)
    @Insert(value = "onCreate" , mayCreateSuper = true)
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        timeRecord.startTime = System.currentTimeMillis();
        Origin.callVoid();
    }

    @TargetClass(value = "androidx.appcompat.app.AppCompatActivity",scope = Scope.ALL)
    @Insert(value = "onWindowFocusChanged" , mayCreateSuper = true)
    public void onWindowFocusChanged(boolean hasFocus) {
        if (hasFocus){
            if (timeRecord.startTime != 0){
                Object o = This.get();
                if (o instanceof Activity){
                    Log.e("object", o.toString() );
                }
                long distance = System.currentTimeMillis() - timeRecord.startTime;
                Log.e("time", This.get().getClass().getName() +" 启动时间 = "+distance );
                timeRecord.startTime = 0;
            }
        }
        Origin.callVoid();
    }

    // 可统计单一方法运行时间
    @TargetClass(value = "com.example.aopproject.MainActivity")
    @Insert(value = "netWork")
    private void netWork(){
        long start = System.currentTimeMillis();
        Origin.callVoid();
        long distance = System.currentTimeMillis() - start;
        Log.e("net","运行时间 = " +distance );
    }
}

参考资料:

AOP——Android通过AspectJ实现登录检验

Android使用AOP做登录拦截

AOP面向切面编程在Android中的使用

自定义注解之运行时注解(RetentionPolicy.RUNTIME)  [可通过该文理解,注解保留策略]

项目完整代码

你可能感兴趣的:(java基础)