阅读 222

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

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

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

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


二、Android中AOP的实现

2.1、Java Annotation的简介

在我们andorid开发中都使用过注解功能,第三方库有注解的有ButterKnif、dagger2、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.2、自定义Annotation加反射实现findViewById功能

自定义Annotation,实现自己的注解

@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代码,避免性能损耗。但是你知道了这些,面试官继续问你这些注解第三方库的原理,也不至于哑口无言!!

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

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

3.1、项目中引用

项目build.gradle中:

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

app里的build.gradle中顶部添加

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

3.2、使用场景:数据埋点

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

//标注我们要通过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.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信息去实现关注");
    }
复制代码

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

4.1、常见标注介绍

  • @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的使用整理

我的公众号

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