一、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为空,快去登录把!!");
}
}
注意点:
- 当Action为Before、After时,方法入参为JoinPoint。
- 当Action为Around时,方法入参为ProceedingPoint。
- 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的使用整理
我的公众号
本人最近也在开始准备面试。费曼学习法,从自己弄明白开始,用浅白的语言叙述。写博客也是这个目的吧。在准备面试资料的同事遇到新知识点,也要各个击破、喜欢的话,可以关注下公众号