注解:大话AOP与Android的爱恨情仇
1. AOP与OOP的区别
平时我接触多的就是OOP(Object Oriented Programming面向对象)、AOP(Aspect Oriented Programming面向切面)这两种编程方式,我用自己的语言来解释一下这两者的区别:
OOP:
专业术语: OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
面向对象侧重静态,名词,状态,组织,数据,载体是空间;大白话: OOP面向对象的三大特征 : 封装 , 继承 , 多态 。这些特征也说明了OOP是面向对象的,我们做什么都是考虑一个对象,我们需要完成一个任务的时候一般都想着把一些操作封装成一个类,所有的变量和操作都封装到一个类里面,那么这个类就是我们的对象,我们要实现某个特定的功能,首先也想想着在这个对象里面去实现。
面向对象也是有明显缺点的,比如我们想实现某些不是常用的功能,我们需要去在需要的对象中去一一实现这些功能,并我们要不断去维护这些功能,一旦多了我们就会很累的。
比如Android中一些按键统计、生命周期统计,特定统计都是比较琐碎的事情,要利用面向对象的思想去实现都不是很完美,这就要求去一一实现,显得很琐碎。
AOP:
专业术语: AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
大白话: AOP面向切面,我在使用的时候是关注具体的方法和功能切入点,不需要知道也不用关心所在什么类或者是什么对象,我们只关注功能的实现,具体对象是谁,爱谁谁!
网上很流行的说明段子:
举个简单的例子,对于“雇员”这样一个业务实体进行封装,自然是OOP/OOD的任务,我们可以为其建立一个“Employee”类,并将“雇员”相关的属性和行为封装其中。而用AOP设计思想对“雇员”进行封装将无从谈起。同样,对于“权限检查”这一动作片断进行划分,则是AOP的目标领域。而通过OOD/OOP对一个动作进行封装,则有点不伦不类。换而言之,OOD/OOP面向名词领域,AOP面向动词领域。
如果说面向对象编程是关注将需求功能划分为不同的并且相对独立,封装良好的类,并让它们有着属于自己的行为,依靠继承和多态等来定义彼此的关系的话;那么面向方面编程则是希望能够将通用需求功能从不相关的类当中分离出来,能够使得很多类共享一个行为,一旦发生变化,不必修改很多类,而只需要修改这个行为即可。
面向方面编程和面向对象编程不但不是互相竞争的技术而且彼此还是很好的互补。面向对象编程主要用于为同一对象层次的公用行为建模。它的弱点是将公共行为应用于多个无关对象模型之间。而这恰恰是面向方面编程适合的地方。有了 AOP,我们可以定义交叉的关系,并将这些关系应用于跨模块的、彼此不同的对象模型。AOP 同时还可以让我们层次化功能性而不是嵌入功能性,从而使得代码有更好的可读性和易于维护。它会和面向对象编程合作得很好。
上面的段子不知道谁说的,很流行,但确实说得很明白!
2. AOP的Java实现方式
上面说过,AOP关注的方法功能点,事先不知道所在对象是谁,当然程序的运行都是需要拿到对象在运行的,要在知道方法功能点的前提下拿到对象并执行,这就需要用到Java的动态代理。
在java的动态代理机制中,有两个重要的类或接口,一个是 InvocationHandler(Interface)、另一个则是 Proxy(Class),我们可以自己写代码去定义自己的动态代理,去实现AOP,但是太麻烦了。Java有很多AOP实现的库,JavaWeb里面有JBoss、SpringFramework、AspectJ等等。Android我熟悉,Android中基于AOP实现的库有ButterKnife、dagger2、EventBus3.0、Retrofit 2.0等等,不是说这些库完全基于AOP实现,只是说里面部分功能基于AOP。
3. Java Annotation
Java Annotation是JDK5.0引入的一种注释机制。Java源代码里面有自己的注解,我们还是很常见的:
java中的Annotation:
@Deprecated -- @Deprecated 所标注内容,不再被建议使用。
@Override -- @Override 只能标注方法,表示该方法覆盖父类中的方法。
@Documented -- @Documented 所标注内容,可以出现在javadoc中。
@Inherited -- @Inherited只能被用来标注“Annotation类型”,它所标注的Annotation具有继承性。
@Retention -- @Retention只能被用来标注“Annotation类型”,而且它被用来指定Annotation的RetentionPolicy属性。
@Target -- @Target只能被用来标注“Annotation类型”,而且它被用来指定Annotation的ElementType属性。
@SuppressWarnings -- @SuppressWarnings 所标注内容产生的警告,编译器会对这些警告保持静默。
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
当然我们也可以自定义注解,只是我们自定的注解需要我们自己去反射或者动态代理来处理注解一次来实现AOP,自己处理还是比较麻烦,反射也是比较耗费性能的,不建议使用,最好是Annotation+APT来做,把注解翻译成Java代码,避免性能损耗,APT后面会说到。
Java Annotation的本质是使用了java的动态代理,Annotation并不代表AOP,Java的AOP实现可以基于Java动态代理,也可以基于Annotation,但是自定义的Annotation需要结合 Java的动态代理才可以实现AOP。
虽然源代码我没看过,JavaWeb的一些库JBoss、SpringFramework、AspectJ等等也是有使用Java Annotation,不可能所有的AOP都是完全自己去用动态代理实现,不然写起来也费劲,用起来也不方便。
Java的Class类中有一系列支持Annotation和反射的实现:
里面有一个很关键的接口:AnnotatedElement,里面是Class反射时对Annotation支持的一系列方法:
具体的功能我就不解释了,自己去查一下。
基于java的Annotation中的Target和Retention结合类似反射原理我们可以实现自己的AOP。很多Android的AOP实现也是这么玩的。
4. Annotation的Android实现方式
Android是用Java写的,上面说到了的上面Java Annotation 动态代理当然都适用于Android,但是Android也有自己的AOP实现方式,但是Android的AOP实现原理跟Java的实现原理是一样的。
再次强调:
Java Annotation的本质是使用了java的动态代理,Annotation并不代表AOP,Java的AOP实现可以基于Java动态代理,也可以基于Annotation,但是自定义的Annotation需要结合 Java的动态代理才可以实现AOP。
4.1 Android自带基于Annotation的AOP实现
Android源码中也有Google官方自定义的AndroidAnnotations,下面是代码结构:
右边代码展示的是Android自带的Keep注解,就是防止代码混淆用到的,可以看到Keep同样是基于Java Annotation自定义的而来的。源代码我没去看,Android自定的注解实现也是基于Annotation和APT(注解处理工具)的AOP操作。
4.2 自定义Annotation加反射实现findViewById
自己的Annotation:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ViewInject{
int value();
}
- 1
- 2
- 3
- 4
- 5
Annotation反射处理工具:
public class AnnotateUtils {
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
ViewInject viewInject = field.getAnnotation(ViewInject.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();
}
}
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
使用示例:
@ViewInject(R.id.buy)
private Button buy;
@ViewInject(R.id.money)
private TextView money;
@ViewInject(R.id.tv_power)
private TextView power;
@ViewInject(R.id.tv_life)
private TextView life;
@ViewInject(R.id.tv_dex)
private TextView dex;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AnnotateUtils.injectViews(this);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
4.3 Android开源库使用Java Annotation
下面是 butterknife使用的注解:
左边是butterknife的注解,右边是最常用的View绑定注解BindView,可以看到也是使用了java的Annotation中的@Retention()和@Target()。
其他的库其实也差不多,这里就不一一截图验证了。
虽然Java Annotation并不是AOP,但是为了方便使用AOP,为了更好的使用我们往往需要使用Annotation来辅助。具体体现就是这些开源库的各种注解。
黄金天然不是货币,而货币天然是黄金,是不是可以这样撕逼一下:Annotation天然不是AOP,而AOP天然是Annotation。
5. 大话AOP与Android的爱恨情仇之AOP三大精钢
三大精钢分别是:APT、AspectJ、Javassist
下面是这三个的作用时间:
5.1 APT
APT用来在编译时期扫描处理源代码中的注解信息,我们可以根据注解信息生成一些文件,比如Java文件。利用APT为我们生成的Java代码,实现冗余的代码功能,这样就减少手动的代码输入,提升了编码效率,而且使源代码看起来更清晰简洁。
Dagger、ButterKnife、AndroidAnnotation、EventBus的注解实现AOP为什么非要使用APT?
如果不使用用APT基于注解动态生成java代码,那么就需要在运行的时候使用反射或者动态代理,那么耗费了不必要的性能,本来就是为了方便的库反而更加耗费性能那就没人用了,使用APT增加了代码量,但是不耗费性能,用起来方便高性能无察觉。
可以说基于Java Annotation的自定义注解配合APT实现了很多简单易用性能优越的AOP用法的开源库。
5.2 AspectJ
-
AspectJ是一个代码生成工具(Code Generator)。
-
AspectJ语法就是用来定义代码生成规则的语法。
Aspectj一个易用的、功能强大的aop编程语言,AspectJ是AOP的Java语言的实现,AspectJ是一个代码生成工具(Code Generator)。
使用AspectJ有两种方法:
- 完全使用AspectJ的语言。这语言一点也不难,和Java几乎一样,也能在AspectJ中调用Java的任何类库。AspectJ只是多了一些关键词罢了。
- 或者使用纯Java语言开发,然后使用AspectJ注解,简称@AspectJ。
使用AspectJ用java开发时使用的AspectJ注解其实也是基于Java Annotation的,下面看结构图:
左边是所有的AspectJ注解,右边是AspectJ注解中最关键的Aspect注解,可以看到也是使用了java的Annotation中的@Retention()和@Target()。
这是我之前做的基于AspectJ做的统计,生命周期监听和click监听:
https://github.com/Dawish/CustomViews/tree/master/Aoplib
请参考牛逼的文章:http://blog.csdn.net/innost/article/details/49387395
需要着重说一下Advice,Advice就是我们插入的代码以何种方式插入,有Before还有After、Around。
关键词 | 说明 |
---|---|
Before | 切入点之前执行,切入点执行之前我们可以先执行我们的方法,可以拦截切入点的执行,比如拦截需要用户支付或者登陆的操作 |
after | 切入点之后执行 ,比如记录用户行为的操作 |
around | 包含切入点的前后,切入点的执行可以控制。比如需要条件判断再执行,执行完成后给用户提示的操作 |
示例:
/**下面第一个*号表示被切入的方法不限定返回类型**/
@Before("execution(* android.view.View.OnClickListener.onClick(..))")
public void onUserAction(JoinPoint joinPoint){
Log.e(TAG, "aop statistics click");
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0){
return;
}
View clickView = (View) args[0];
//你想做的操作
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
上面是在执行onClick之前切入,我们可以根据JoinPoint 来获取onClick(View v)方法本身的参数,来对点击的View做操作,我们也可以根据用户的连续点击间隔时间来防止点击速度过快导致的双击。
是不是很强大。
我们可以用JoinPoint 参数来获取更多的内容:
- java.lang.Object[] getArgs():获取连接点方法运行时的入参列表;
- Signature getSignature() :获取连接点的方法签名对象;
- java.lang.Object getTarget() :获取连接点所在的目标对象;
-
java.lang.Object getThis() :获取代理对象本身;
有了Before、After、Around和JoinPoint 的四者配合,真是6得飞起,可上天入地啊,可以很方便完成Android开发中很恶心很繁琐的任务。
来,我们走一个!
5.3 Javassist
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba
(千叶 滋)所创建的。它已加入了开放源代码JBoss
应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态”AOP”框架。 – 百度百科
代表框架:热修复框架HotFix 、Savior(InstantRun)等
Javassist作用是在编译器间修改class文件,与之相似的ASM(热修复框架女娲)也有这个功能,可以让我们直接修改编译后的class二进制代码,首先我们得知道什么时候编译完成,并且我们要赶在class文件被转化为dex文件之前去修改。在Transfrom这个api出来之前,想要在项目被打包成dex之前对class进行操作,必须自定义一个Task,然后插入到predex或者dex之前,在自定义的Task中可以使用javassist或者asm对class进行操作。而Transform则更为方便,Transfrom会有他自己的执行时机,不需要我们插入到某个Task前面。Tranfrom一经注册便会自动添加到Task执行序列中,并且正好是项目被打包成dex之前。 来自— http://www.jianshu.com/p/dca3e2c8608a
Android热补丁动态修复技术请参考:http://blog.csdn.net/u010386612/article/details/51131642
Javassist我本身不是很熟,我就不多说了,反正都TMD超级牛逼的东西。
本人家技术有限,上面很多都是自己基于自己的知识面说的,不足不对之处还望各位同行不吝指教。