Android中的AOP的实现及AspectJ的使用

1、OOP和AOP的简单简介和区别

OOP(Object Oriented Programming): 这就是咱们android中的面向对象开发。面向对象的三大特征,封装、继承和多态。这里很少赘述。java

AOP(Aspect Oriented Programming):面向切面编程;AOP则是面对业务逻辑处理过程当中的切面进行提取,也就是程序处理的某个步骤或者阶段,以达到代码间的低耦合、代码分离、提升代码重用性android


2、Android中AOP的实现

2.一、Java Annotation的简介

在咱们andorid开发中都使用过注解功能,第三方库有注解的有ButterKnif、dagger二、EventBus、Retrofit,其实这些库部分核心功能也是基于AOP实现的。只不过他们还用到了其余插件,好比APT,APT在程序编译期,扫描代码中的注解信息,并为咱们生成java代码,实现咱们的功能,无需咱们手动去处理。面试

Java Annotation是JDK5.0引入的注解机制。在咱们代码里。常常能够看到@Override:表示方法覆盖父类方法。编程

java中的Annotation:

@Deprecated  --  所标注内容,再也不被建议使用。
@Override    --  只能标注方法,表示该方法覆盖父类中的方法。
@Documented  --  所标注内容,能够出如今javadoc中。
@Inherited   --  只能被用来标注“Annotation类型”,它所标注的Annotation具备继承性。
@Retention   --  只能被用来标注“Annotation类型”,并且它被用来指定Annotation的RetentionPolicy属性。
@Target      --  只能被用来标注“Annotation类型”,并且它被用来指定Annotation的ElementType属性。
@SuppressWarnings --  所标注内容产生的警告,编译器会对这些警告保持静默。
复制代码

2.二、自定义Annotation加反射实现findViewById功能

自定义Annotation,实现本身的注解markdown

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInject {
    int value();
}
复制代码

MyInject的反射处理工具
public class MyInjectUtils {
    public static void injectViews(Activity activity) {
        Class<? extends Activity> object = activity.getClass(); // 获取activity的Class
        Field[] fields = object.getDeclaredFields(); // 经过Class获取activity的全部字段
        for (Field field : fields) { // 遍历全部字段
            // 获取字段的注解,若是没有ViewInject注解,则返回null
            MyInject viewInject = field.getAnnotation(MyInject.class);
            if (viewInject != null) {
                int viewId = viewInject.value(); // 获取字段注解的参数,这就是咱们传进去控件Id
                if (viewId != -1) {
                    try {
                        // 获取类中的findViewById方法,参数为int
                        Method method = object.getMethod("findViewById", int.class);
                        // 执行该方法,返回一个Object类型的View实例
                        Object resView = method.invoke(activity, viewId);
                        field.setAccessible(true);
                        // 把字段的值设置为该View的实例
                        field.set(activity, resView);
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
复制代码

在Activity里的使用
@MyInject(R.id.button)
    Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyInjectUtils.injectViews(this);
    }
复制代码

这样咱们就实现了findViewById的功能了。不难发现,此功能和ButterKnif的findViewById很是类似,可是有本质的区别。由于咱们采用了反射,在android中是很是消耗性能的。因此那些第三方库则会采起Annotation+APT来作,把注解译成Java代码,避免性能损耗。可是你知道了这些,面试官继续问你这些注解第三方库的原理,也不至于哑口无言!!app

3、AspectJ的使用及使用场景等(重点)

AspectJ:是一个代码生成工具,AspectJ语法就是用来定义代码生成规则的语法ide

3.一、项目中引用

项目build.gradle中:函数

dependencies {
        //...
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.8'
    }
复制代码

app里的build.gradle中顶部添加工具

apply plugin: 'android-aspectjx'
复制代码

3.二、使用场景:数据埋点

一般咱们数据埋点都会经过Application中的registerActivityLifecycleCallbacks监听。但这里咱们使用AspectJ。代码以下(这里的标注@Before、@After,关键字execution,call后面详细讲解,这里咱们先把功能实现了):oop

//标注咱们要经过Aspect语法生成代码的辅助类
@Aspect
public class AspectHelper {
    private final String TAG = this.getClass().getSimpleName();
    
    //com.lihang.aoptestpro.BaseActivity 是我项目里的BaseActivity
    //这句代码实现的功能是:会打印咱们项目里全部Activity里全部的on开头的方法
    //joinPoint.getThis().getClass().getSimpleName() 当前Activity的类名
    @Before("execution(* com.lihang.aoptestpro.BaseActivity.on**(..))")
    public void onActivityStart(JoinPoint joinPoint) throws Throwable {
        String key = joinPoint.getSignature().toString();
        Log.i(TAG, key + "============" + joinPoint.getThis().getClass().getSimpleName());
    }

    //会打印咱们项目里全部Activity里的onPause方法。
    @Before("execution(* com.lihang.aoptestpro.BaseActivity.onPause(..))")
    public void onActivityPause(JoinPoint joinPoint) throws Throwable {
        String key = joinPoint.getSignature().toString();
        Log.i(TAG, key + "============" + joinPoint.getThis().getClass().getSimpleName());
    }
}
复制代码

至此,拿到全部Activity的生命周期,埋点功能就能够实现了;注意及总结:

  • 切忌勿用
@Before("execution(* android.app.Activity.on**(..))")
复制代码

网上大部分都是用这句,实践发现会除了会走咱们的Activity还会走系统的Activity及FragmentActivity.至少3次


  • 使用BaseActivity
@Before("execution(* com.lihang.aoptestpro.BaseActivity.on**(..))")
复制代码

若是你BaseActivity不去实现系统生命周期,你会发现根本不走。因此好比要抓onStart、onPause生命周期时,必定要在BaseActivity去实现,即便方法体内是空也行

其实这里还能用下面的语法实现,前提是你全部的Activity必须以“Activity”字符串做为类名的尾部

@Before("execution(* com.lihang.aoptestpro.*Activity.on**(..))")
复制代码

3.三、使用场景:登陆校验

咱们在项目开发时,有些功能每每须要登陆后才能使用,若是没有登陆,就去跳转登陆页面。这样就避免不了if/else的判断。以下,点击关注时的代码

public void follow() {
        if (MyApplication.getInstance().getLoginUser() != null) {
            User user = MyApplication.getInstance().getLoginUser();
            Log.i(TAG, "已登陆,user不为空,用user信息去实现关注");
        } else {
            Log.i(TAG, "未登陆,跳转登陆页面");
        }
    }
复制代码

那么使用AOP非侵入式怎么使用呢? 首先咱们先定义个标注

@Target(ElementType.METHOD)//这里是标注方法,以前那个Filed是标注属性
@Retention(RetentionPolicy.RUNTIME)
public @interface IsLogin {
}
复制代码

而后看咱们的Aspect里:

@Aspect
public class AspectHelper {
    private final String TAG = this.getClass().getSimpleName();
    
    @Around("execution(@com.lihang.aoptestpro.IsLogin * *(..))")
    public void isLoginOn(ProceedingJoinPoint joinPoint) throws Throwable {
        if (MyApplication.getInstance().getLoginUser() != null) {
            //joinPoint.proceed()能够当作就是咱们用@IsLogin标注的那个方法,调用proceed意思就是继续执行方法
            //这里的意思就是全部用标注@IsLogin标注的,是登陆状态才会继续执行方法,不然会执行咱们下面的去登陆,不会执行原方法
            joinPoint.proceed();
        } else {
            Log.i(TAG, "user为空,快去登陆把!!");
        }
    }
}
复制代码

而后再看看咱们的follow方法。用@IsLogin标注后,就能够直接处理登陆状态就好了。真的是低耦合,代码复用性高

@IsLogin
    public void follow() {
        User user = MyApplication.getInstance().getLoginUser();
        Log.i(TAG, "已登陆,user不为空,用user信息去实现关注");
    }
复制代码

4、AspectJ常见关键字以及各自的区别

4.一、常见标注介绍

  • @Before:意思就是在方法以前执行
//意思是onActivityPause会在BaseActivity.onPause()方法前执行
    @Before("execution(* com.lihang.aoptestpro.BaseActivity.onPause(..))")
    public void onActivityPause(JoinPoint joinPoint) throws Throwable {

    }
复制代码
  • @After:同理则是在方法后执行
  • @Around:包括了@Befor和@After的功能,其能够进行控制
//joinPoint.proceed()是控制方法是否继续往下执行
    //在joinPoint.proceed()前的逻辑代码,就是实现@Before的功能,在方法前执行
    //在joinPoint.proceed()后的逻辑代码,就是实现@After的功能,在方法后执行
    @Around("execution(@com.lihang.aoptestpro.IsLogin * *(..))")
    public void isLoginOn(ProceedingJoinPoint joinPoint) throws Throwable {
        if (MyApplication.getInstance().getLoginUser() != null) {
            joinPoint.proceed();
        } else {
            Log.i("MainActivity", "user为空,快去登陆把!!");
        }
    }
复制代码

注意点:

  1. 当Action为Before、After时,方法入参为JoinPoint。
  2. 当Action为Around时,方法入参为ProceedingPoint。
  3. Around和Before、After的最大区别: ProceedingPoint不一样于JoinPoint,其提供了proceed方法执行目标方法。

4.2常见关键字介绍

翻阅了大量资料,一样的代码。从其生成的代码里看。

  • call:插入在函数体体外

简单看就是:

Call(Before)
Pointcut{
   Pointcut Method
}
Call(After)
复制代码

  • execution:插入在函数体内

简单看就是:

Pointcut{
  execution(Before)
    Pointcut Method
  execution(After)
}
复制代码

虽然知道其工做原理了。但做者也存在一个疑问,那就是什么call和execution都能实现同一的功能。可是什么场景使用哪一个更佳呢?但愿有知道的小伙伴帮忙解答下

参考文献

看 AspectJ 在 Android 中的强势插入
大话AOP与Android的爱恨情仇
Android 自动化埋点:基于AspectJ的沪江SDK的使用整理

个人公众号

本人最近也在开始准备面试。费曼学习法,从本身弄明白开始,用浅白的语言叙述。写博客也是这个目的吧。在准备面试资料的同事遇到新知识点,也要各个击破、喜欢的话,能够关注下公众号

相关文章
相关标签/搜索