Android编译时注解框架从入门到项目实践。该系列将经过5篇博客一步步教你打造一个属于本身的编译时注解框架,并在以后开源出基于APT的编译时注解框架。java
提到注解,广泛都会有两种态度:黑科技、低性能。使用注解每每能够实现用很是少的代码做出匪夷所思的事情,好比这些框架:ButterKnife、Retrofit。但一直被人诟病的是,运行时注解会由于java反射而引发较为严重的性能问题...android
今天咱们要讲的是,不会对性能有任何影响的黑科技:编译时注解。也有人叫它代码生成,其实他们仍是有些区别的,在编译时对注解作处理,经过注解,获取必要信息,在项目中生成代码,运行时调用,和直接运行手写代码没有任何区别。而更准确的叫法:APT - Annotation Processing Toolgit
得当的使用编译时注解,能够极大的提升开发效率,避免编写重复、易错的代码。大部分时候编译时注解均可以代替java反射,利用能够直接调用的代码代替反射,极大的提高运行效率。github
本章做为《Android编译时注解框架》系列的第一章,将分三个部分让你简单认识注解框架。以后咱们会一步步的建立属于本身的编译时注解框架。数组
什么是注解app
运行时注解的简单使用框架
编译时注解框架ButterKnife源码初探ide
注解你必定不会陌生,这就是咱们最多见的注解:布局
首先注解分为三类:性能
标准 Annotation
包括 Override, Deprecated, SuppressWarnings,是java自带的几个注解,他们由编译器来识别,不会进行编译, 不影响代码运行,至于他们的含义不是这篇博客的重点,这里再也不讲述。
元 Annotation
@Retention, @Target, @Inherited, @Documented,它们是用来定义 Annotation 的 Annotation。也就是当咱们要自定义注解时,须要使用它们。
自定义 Annotation
根据须要,自定义的Annotation。而自定义的方式,下面咱们会讲到。
一样,自定义的注解也分为三类,经过元Annotation - @Retention 定义:
@Retention(RetentionPolicy.SOURCE)
源码时注解,通常用来做为编译器标记。如Override, Deprecated, SuppressWarnings。
@Retention(RetentionPolicy.RUNTIME)
运行时注解,在运行时经过反射去识别的注解。
@Retention(RetentionPolicy.CLASS)
编译时注解,在编译时被识别并处理的注解,这是本章重点。
运行时注解的实质是,在代码中经过注解进行标记,运行时经过反射寻找标记进行某种处理。而运行时注解一直以来被呕病的缘由即是反射的低效。
下面展现一个Demo。其功能是经过注解实现布局文件的设置。
以前咱们是这样设置布局文件的:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home);
}
复制代码
若是使用注解,咱们就能够这样设置布局了
@ContentView(R.layout.activity_home)
public class HomeActivity extends BaseActivity {
。。。
}
复制代码
咱们先不讲这两种方式哪一个好哪一个坏,咱们只谈技术不谈需求。
那么这样的注解是怎么实现的呢?很简单,往下看。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ContentView {
int value();
}
复制代码
第一行:@Retention(RetentionPolicy.RUNTIME)
@Retention用来修饰这是一个什么类型的注解。这里表示该注解是一个运行时注解。这样APT就知道啥时候处理这个注解了。
第二行:@Target({ElementType.TYPE})
@Target用来表示这个注解可使用在哪些地方。好比:类、方法、属性、接口等等。这里ElementType.TYPE 表示这个注解能够用来修饰:Class, interface or enum declaration。当你用ContentView修饰一个方法时,编译器会提示错误。
第三行:public @interface ContentView
这里的interface并非说ContentView是一个接口。就像申明类用关键字class。申明枚举用enum。申明注解用的就是@interface。(值得注意的是:在ElementType的分类中,class、interface、Annotation、enum同属一类为Type,而且从官方注解来看,彷佛interface是包含@interface的)
/** Class, interface (including annotation type), or enum declaration */
TYPE,
复制代码
第四行:int value();
返回值表示这个注解里能够存放什么类型值。好比咱们是这样使用的
@ContentView(R.layout.activity_home)
复制代码
R.layout.activity_home实质是一个int型id,若是这样用就会报错:
@ContentView(“string”)
复制代码
关于注解的具体语法,这篇不在详述,统一放到《Android编译时注解框架-语法讲解》中
注解申明好了,但具体是怎么识别这个注解并使用的呢?
@ContentView(R.layout.activity_home)
public class HomeActivity extends BaseActivity {
。。。
}
复制代码
注解的解析就在BaseActivity中。咱们看一下BaseActivity代码
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//注解解析
for (Class c = this.getClass(); c != Context.class; c = c.getSuperclass()) {
ContentView annotation = (ContentView) c.getAnnotation(ContentView.class);
if (annotation != null) {
try {
this.setContentView(annotation.value());
} catch (RuntimeException e) {
e.printStackTrace();
}
return;
}
}
}
复制代码
第一步:遍历全部的子类
第二步:找到修饰了注解ContentView的类
第三步:获取ContentView的属性值。
第四步:为Activity设置布局。
相信你如今对运行时注解的使用必定有了一些理解了。也知道了运行时注解被人呕病的地方在哪了。
你可能会以为*setContentView(R.layout.activity_home)和@ContentView(R.layout.activity_home)*没什么区别,用了注解反而还增长了性能问题。
但你要知道,这只是注解最简单的应用方式。举一个例子:AndroidEventBus的注解是运行时注解,虽然会有一点性能问题,可是在开发效率上是有提升的。
由于这篇博客的重点不是运行时注解,因此咱们不对其源码进行解析。有兴趣的能够去github搜一下看看哦~话说AndroidEventBus仍是我一个学长写得,haha~。
ButterKnife你们应该都很熟悉的吧,9000多颗start,让咱们完全告别了枯燥的findViewbyId。它的使用方式是这样的:
你难道就没有好奇过,它是怎么实现的吗?嘿嘿,这就是编译时注解-代码生成的黑科技所在了。
秘密在这里,编译工程后,打开你的项目app目录下的build目录:
你能够看到一些带有*$$ViewBinder*后缀的类文件。这个就是ButterKnife生成的代码咱们打开它:
上面有一条注释: // Generated code from Butter Knife. Do not modify!
1.ForgetActivity$$ViewBinder 和 咱们的 ForgetActivity同在一个包下:
package com.zhaoxuan.wehome.view.activity;
复制代码
同在一个包下的意义是什么呢?ForgetActivity$$ViewBinder 能够直接使用 ForgetActivity protected级别以上的属性方法。就像这样:
//accountEdit是ForgetActivity当中定义的控件
target.accountEdit = finder.castView(view, 2131558541, "field 'accountEdit'");
复制代码
因此你也应该知道了为何当使用private时会报错了吧?
2.咱们不去管细节,只是大概看一下这段生成的代码是什么意思。我把解析写在注释里。
@Override
public void bind(final Finder finder, final T target, Object source) {
//定义了一个View对象引用,这个对象引用被重复使用了(这但是一个偷懒的写法哦~)
View view;
//暂时无论Finder是个什么东西,反正就是一种相似于findViewById的操做。
view = finder.findRequiredView(source, 2131558541, "field 'accountEdit'");
//target就是咱们的ForgetActivity,为ForgetActivity中的accountEdit赋值
target.accountEdit = finder.castView(view, 2131558541, "field 'accountEdit'");
view = finder.findRequiredView(source, 2131558543, "field 'forgetBtn' and method 'forgetOnClick'");
target.forgetBtn = finder.castView(view, 2131558543, "field 'forgetBtn'");
//给view设置一个点击事件
view.setOnClickListener(
new butterknife.internal.DebouncingOnClickListener() {
@Override
public void doClick(android.view.View p0) {
//forgetOnClick()就是咱们在ForgetActivity中写得事件方法。
target.forgetOnClick();
}
});
}
复制代码
OK,如今你大体明白了ButterKnife的秘密了吧?经过自动生成代码的方式来代替咱们去写findViewById这样繁琐的代码。如今你必定在疑惑两个问题:
1.这个bind方法何时被调用?咱们的代码里并无ForgetActivity$$ViewBinder 这种奇怪的类引用呀。
2.Finder究竟是个什么东西?凭什么它能够找到view。
不着急不着急,慢慢看。
咱们能够解读的信息以下:
Bind是编译时注解
只能修饰属性
属性值是一个int型的数组。
建立好自定义注解,以后咱们就能够经过APT去识别解析到这些注解,而且能够经过这些注解获得注解的值、注解所修饰的类的类型、名称。注解所在类的名称等等信息。
经过上面生成的代码,你必定奇怪,Finder究竟是个什么东西。Finder实际是一个枚举。
根据不一样类型的,提供了不一样实现的findView和getContext方法。这里你终于看到了熟悉的findViewById了吧,哈哈,秘密就在这里。
另外Finder还有两个重要的方法,也是刚才没有介绍清楚的: finder.findRequiredView 和 finder.castView
findRequiredView 方法调用了 findOptionalView 方法
findOptionalView调用了不一样枚举类实现的findView方法(实际上就是findViewById啦~)
findView取得view后,又交给了castView作一些容错处理。
castView上来啥都不干直接强转并return。若是发生异常,就执行catch方法,只是抛出异常而已,咱们就不看了。
*ButterKnife.bind(this)*这个方法咱们一般都在BaseActivity的onCreate方法中调用,彷佛全部的findViewById方法,都被这一个bind方法化解了~
bind有几个重载方法,但最终调的都是下面这个方法。
参数target通常是咱们的Activity,source是用来获取Context查找资源的。当target是activity时,Finder是Finder.ACTIVITY。
首先取得target,(也就是Activity)的Class对象,根据Class对象找到生成的类,例如:ForgetActivity$$ViewBinder。
而后调用ForgetActivity$$ViewBinder的bind方法。
而后就没有啦~看到这里你就大体明白了在程序运行过程当中ButterKnife的实现原理了。下面上重头戏,ButterKnife编译时所作的工做。
你可能在疑惑,ButterKnife是如何识别注解的,又是如何生成代码的。
AbstractProcessor是APT的核心类,全部的黑科技,都产生在这里。AbstractProcessor只有两个最重要的方法process 和 getSupportedAnnotationTypes。
重写getSupportedAnnotationTypes方法,用来表示该AbstractProcessor类处理哪些注解。
第一个最明显的就是Bind注解啦。
而全部的注解处理,都是在process中执行的:
经过findAndParseTargets方法拿到全部须要被处理的注解集合。而后对其进行遍历。
JavaFileObject是咱们代码生成的关键对象,它的做用是写java文件。ForgetActivity$$ViewBinder这种奇怪的类文件,就是用JavaFileObject来生成的。
这里咱们只关注最重要的一句话
writer.write(bindingClass.brewJava());
复制代码
ForgetActivity$$ViewBinder中全部代码,都是经过bindingClass.brewJava方法拼出来的。
哎,我不知道你看到这个代码时候,是什么感受。反正我看到这个时候脑壳里只有一句话:好low啊……
我根本没想到这么黑科技高大上的东西竟然是这么写出来的。一行代码一行代码往出拼啊……
既然知道是字符串拼接起来的,就没有看下去的心思了,这里就不放完整代码了。
由此,你也知道了以前看生成的代码,为何是用了偷懒的方法写了吧~
当你揭开一个不熟悉领域的面纱后,黑科技好像也不过如此,甚至用字符串拼接出来的代码感受lowlow的。
但这不正是学习的魅力么?
好了,总结一下。
编译时注解的魅力在于:编译时按照必定策略生成代码,避免编写重复代码,提升开发效率,且不影响性能。
代码生成与代码插入(Aspectj)是有区别的。代码插入面向切面,是在代码运行先后插入代码,新产生的代码是由原有代码触发的。而代码生成只是自动产生一套独立的代码,代码的执行仍是须要主动调用才能够。
APT是一套很是强大的机制,它惟一的限制在于你天马行空的设计~
ButterKnife的原理其实很简单,但是为何这么简单的功能,却写了那么多代码呢?由于ButterKnife做为一个外部依赖框架,作了大量的容错和效验来保证运行稳定。因此:写一个框架最难的不是技术实现,而是稳定!
ButterKnife有一个很是值得借鉴的地方,就是如何用生成的代码对已有的代码进行代理执行。这个若是你在研究有代理功能的APT框架的话,应该好好研究一下。
APT就好像一块蛋糕摆在你面前,就看你如何优雅的吃了。
后续篇章我将会陆续推出几款以Cake命名的APT框架。