写在前面
下面是一些关于注解的个人认识,可以跳过,直接从知识点
部分看起。
自从注解出现以后,很多框架都喜欢用它来干活,显得轻便优雅。我最早邂逅的还是@Override
这个家伙,那时对一些语言特性还不了解,觉得很怪。然而一旦接受了这个设定,还是挺带感的。现在看不到它还会浑身难受。
一开始,注解是为编译时检查服务的,不会影响程序运行,反而增强了程序的可读性。常见的有:
@Override
: 检查是否正确重写@Deprecated
: 表示该函数已弃用,会划一条横线@NonNull
: 检查参数非空。空指针可以在编译时发现,而不必等到运行时抛出NullPointerException
再到后来,人们开始用反射解析注解了,也就是说,它开始干涉程序的运行。大量的反射,带来大量的性能损耗。尤其在移动端,性能尤为重要,许多开源框架对注解的支持慎之又慎。
不过,事情有了转机。java官方可能是出于注解性能考虑,很快在java_1.6推出了”注解处理器”这个东西。注解处理器,即 annotation processor tool
,简称apt
。它使得我们能在“编译时”处理注解,既保留了注解的功能,又不会在“运行时”造成性能损耗。
第一次接触apt
是在使用ButterKnife
的时候。我一看,嘿,编译时注解、自动生成样板代码findViewById()...
,这么厉害,研究一下。这的确和运行时通过反射处理注解,是完全不同的路子,能达到一些意想不到的效果。
之后,我在网上找了一些类似的框架,也试着自己实践了一把。我的感悟是:就如同AOP
一样,这确实是一种全新的思路,以后不一定用得到,但能拓宽思路。不过:切忌拿着锤子看什么都是钉子,切忌滥用注解。
说了这么多,有请今天的主角,注解处理器apt
!
知识点
本文将通过一个简单的场景,介绍如何使用apt
解析注解,并生成样板代码。包含知识点:
- 自定义注解
- apt相关类
AbstractProcessor
的使用 - 反射体系的亲戚:编译时类型
javax.lang.model
包 - 代码生成工具:
javapoet
源码下载
https://github.com/fashare2015/apt_for_viewholder
需求
用过ButterKnife
都知道,一行@BindView(R.id.rv) RecyclerView mRv;
,会在app/build/generated/source/apt/
下面,生成相关代码:
public class MainActivity_ViewBinding<T extends MainActivity> implements Unbinder {
protected T target;
public MainActivity_ViewBinding(T target, Finder finder, Object source) {
this.target = target;
target.mRv = finder.findRequiredViewAsType(source, R.id.rv, "field 'mRv'", RecyclerView.class);
}
...
}
那我们能做什么呢?我有一个绝妙的主意:通过注解生成ViewHolder
,自动完成如下功能,不限于:
- bind layout: View.inflate(context, R.layout.XXX, null);
- bind id: XXX_View = itemView.findViewById(R.id.XXX);
- bind data: XXX_View.setText(data);
// 注解定义:
@ViewHolder( ... )
private class MyAdapter extends RecyclerView.Adapter{}
// 自动生成:(最终效果)
public class MyAdapter$$ViewHolder extends ViewHolder {
public TextView mTextView_16908308;
public MyAdapter$$ViewHolder(Context context) {
super(View.inflate(context, 17367043, null));
mTextView_16908308 = (TextView)itemView.findViewById(16908308);
}
public void bind(String data) {
mTextView_16908308.setText(data);
}
}
实战
理解了需求之后,我们开始干活吧:
项目结构
- apt_for_viewholder
- app: android application,依赖
apt
模块,用于测试效果 - apt: java library,用于实现功能
- app: android application,依赖
这里有一个坑要说:为啥一定要分离出一个 java lib:apt
?这是由于我们之后会用到javax.lang.model
包,而在android
平台下没有这个包。stackoverflow
上又网友如此吐槽:
当你开始在android平台下进行开发的时候,就要做好用不了标准java库的觉悟。
我想也是,android 迟迟不支持 java8,搞的我都想弃坑了。推出的java8御用编译器jack
,还tm和apt不兼容(黑人问号脸???)
看一下apt/build.gradle
配置:
apply plugin: 'java'
apply plugin: 'me.tatarka.retrolambda'
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
// java 代码生成器: java-1.7, 由 square 开发
compile 'com.squareup:javapoet:1.7.0'
// 第三方 stream
compile 'com.annimon:stream:1.1.3'
}
工欲善其事,必先利其器。用到的工具有:
- java8相关:
annimon:stream
,tatarka.retrolambda
- 代码生成相关:
squareup:javapoet
下面真的真的要开干啦:
自定义注解
为了获取必要信息,我们定义一个注解@ViewHolder
,它可以标注在类(如Adapter)上:
@Target(TYPE) // TYPE: 类或接口; FIELD: 成员变量; METHOD: 方法; ...
@Retention(CLASS) // CLASS: 编译时; RUNTIME: 运行时
public @interface ViewHolder {
int layoutRes(); // 布局资源: R.layout.XXX
}
自定义注解有几点要注意:
- 接受类型:
- 所有基本类型(int,float,boolean,byte,double,char,long,short)
- String
- Class
- enum
- Annotation,这个意思是:注解可以嵌套!!!
- 默认值
- 用关键字
default
表示,不可为 null !!!
- 用关键字
apt相关类:AbstractProcessor
的使用
好了让我们来解析这个注解:
先给一篇介绍全面文章:java 注解处理器
接口介绍
我看完它以后,有了基本的认识,打算讲一讲自己的理解。
核心类是AbstractProcessor
,我们通过继承它来实现自己的功能,主要看几个回调:
- 初始化:
init()
相当于onCreate()
- 最关键的是:
process()
相当于onResume()
- 其它:
getSupportedXXX()
,一些配置:指定 java 版本,指定待解析的注解。。。
另外也可以通过相应的注解配置,如@SupportedXXX(...)
public abstract class AbstractProcessor implements Processor {
protected ProcessingEnvironment processingEnv;
public Set<String> getSupportedOptions() {}
// 指定"目标注解"类型,如 "com.example.annotation.ViewHolder"
public Set<String> getSupportedAnnotationTypes() {}
// 指定 java 版本,如 SourceVersion.RELEASE_8
public SourceVersion getSupportedSourceVersion() {}
// 初始化一些变量:从 processingEnv 中,可以拿到一些有用的工具,之后介绍
public synchronized void init(ProcessingEnvironment processingEnv) {}
// 解析入口,可以理解为main().
// annotations: getSupportedAnnotationTypes() 中指定的 "目标注解" 类型
// roundEnv: 一次"处理循环",可以拿到所有带有 "目标注解" 的 Element.
public abstract boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv);
具体使用
一般来讲,在process()
里处理自己的业务逻辑即可。看一下我是怎么做的:
//@AutoService(Processor.class)
@SupportedAnnotationTypes({
"com.example.annotation.ViewHolder", // 指定 @ViewHolder 为 "目标注解"
"com.example.annotation.Adapter"
})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MyProcessor extends AbstractProcessor{
public static Elements sElementUtils; // Element 处理工具
private static Filer sFiler; // sFiler, 文件 io 读写工具
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
// 两个工具都在 processingEnv 里
sElementUtils = processingEnv.getElementUtils();
sFiler = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 打印一些 "目标注解", 会在 gradle build 期间打印.
// 会打印在 Gradle Console 里,更多打印功能可以用 Messager 工具.
// processingEnv.getMessager()
System.out.println("process: ");
Stream.of(annotations).forEach(System.out:: println);
// 分两步:
// 1. 获取"带目标注解"的class信息
List<ClassInfo> infoList = getClassInfoList(annotations, roundEnv);
// 2. 根据这个class信息,和目标注解包含的信息,生成 java code.
generateJavaCode(infoList);
return true;
}
private List<ClassInfo> getClassInfoList(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
...
}
public static void generateJavaCode(List<ClassInfo> infoList) {
...
}
}
注册 MyProcessor
这一步类似我们在AndroidManifest.xml
里注册Activity
。虽然感觉很怪,但是它是apt
能找到你的MyProcessor
的必要条件。
如图,新建一个
resources/
目录,与java/
目录同级,在里边建一个javax.annotation.processing.Processor
文件
如图,在该文件里写上
MyProcessor
的路径。这样就注册好了。
如果觉得麻烦,也可以用 google 的 auto-service
来自动注册。有兴趣自行百度。
反射体系的亲戚:编译时类型javax.lang.model
包
我们来看process()的第一个参数Set<? extends TypeElement> annotations
。我们给的是Set<String> supportedAnnotationNames
,返回一个TypeElement
的集合,什么鬼???为啥不返回Set<Annotation> 或者 Set< Class<? extends Annotation> >
,起码这两者我们更熟悉。
请注意,现在是”编译时”, Class 什么的存不存在都很难说!!!
所以, java提供了一个javax.lang.model
包,来描述编译时
的信息。与之相对的是我们更为熟悉的java.lang.reflect
包,用以描述运行时
的信息。两个包在某种程度上有很大的相似性。
这个javax.lang.model
包里,有两个重要的类TypeMirror
和Element
。它们共同保存了一个代码片段的信息,可以通过asElement()
和asType()
相互转化。
AnnotatedConstruct
TypeMirror
- DeclaredType: 对应 TypeElement,表示类的信息
- …
Element:代码块未编译前的样子,可以是Feild、Class、Method…
- PackageElement : 代码段 import XXX
- TypeElement:代码段 class XXX{…} 或 interface XXX{…}
- VariableElement: 代码段 String XXX = xxx;
- ExecutableElement: 代码段 函数部分
- …
这段介绍可能有点枯燥,但是我们得到了重要的信息:
使用
AbstractProcessor
时,禁止使用Class
以及其它reflect
相关接口,而采用TypeMirror
和Element
作为代替。以获得getSimpleName()
之类的东西。
代码生成工具:javapoet
介绍
为啥用它呢,其实你有System.out.println()
就完全够了,只需要重定向输出流到某个文件里。当然一些细节会蛋疼死你,有轮子何不用呢?javapoet
,嘿,java诗人
,用起来还真像写诗呢。
输出一个HelloWorld
官方的sample
如下(很好理解):
https://github.com/square/javapoet
照着例子,代码copy
进process()
,把最后的javaFile.writeTo(System.out);
改成javaFile.writeTo(mFiler);
就完事了。我们成功生成了HelloWorld.java
,是不是很开心!!!
你说,妈蛋骗人,我的ViewHolder
呢?别急,我们只需把相关信息按如下级别,填充到对应的位置即可:
- MethodSpec: 函数级别,我们生成一个构造函数试试
- TypeSepc: 类级别,需要类名、继承关系的信息
- JavaFile: 包名,”com.example.XXX”
使用
我弄了两个接口Builder
、AbstractBuilder
,来辅助这些配置:
public interface Builder<Output> {
Output build();
}
// 意为 "输入 Data, 输出相关配置 Output"
// 子类通过 getData(), 获得输入信息
public abstract class AbstractBuilder<Data, Output> implements Builder<Output> {
private Data mData;
public Data getData() {
return mData;
}
public AbstractBuilder(Data data) {
mData = data;
}
}
// 几个级别的 Builder 抽象
public abstract class MethodBuilder<Data> extends AbstractBuilder<Data, MethodSpec>{}
public abstract class ClassBuilder<Data> extends AbstractBuilder<Data, TypeSpec>{}
public class FileBuilder extends AbstractBuilder<String, JavaFile> {}
先别管信息怎么拿到,直接看最终的样板代码:
(这些配置类,我内聚在ViewHolderInfo
类中。)
先配置
Construstor
需要的信息getData().getLayoutRes()
private static class Constructor extends MethodBuilder<ViewHolderInfo> { public Constructor(ViewHolderInfo viewHolderInfo) { super(viewHolderInfo); } // 下面的 getData() 即为传进来的 viewHolderInfo, // 意为 "输入 viewHolderInfo, 输出相关配置" @Override public MethodSpec build() { MethodSpec.Builder builder = MethodSpec.constructorBuilder() // 构造函数 .addModifiers(Modifier.PUBLIC) // public .addParameter(ClassNames.CONTEXT, "context") // 参数 .addStatement( "super($T.inflate(context, $L, null))", ClassNames.VIEW, getData().getLayoutRes() ); // 实现体 return builder.build(); } }
再配置
Clazz
:
需要的信息getData().getSimpleName()
private static class Clazz extends ClassBuilder<ViewHolderInfo> { public Clazz(ViewHolderInfo viewHolderInfo) { super(viewHolderInfo); } // 下面的 getData() 即为传进来的 viewHolderInfo, // 意为 "输入 viewHolderInfo, 输出相关配置" @Override public TypeSpec build() { return TypeSpec.classBuilder(getData().getSimpleName()) // 类名 .superclass(ClassNames.VIEW_HOLDER) // 父类 .addModifiers(Modifier.PUBLIC) // 修饰符 .addMethods(Stream.of(getMethodBuilderList()) .map(MethodBuilder:: build) // 把之前 Constructor 的配置搞进来 .collect(Collectors.toList()) ) // 方法 .build(); } protected List<? extends MethodBuilder<ViewHolderInfo>> getMethodBuilderList() { return Arrays.asList( new Constructor(getData()), // 上面的构造函数配置 new Bind(getData()) // 另外的 Bind 函数。。。 ); } }
最后配置
FileBuilder
:
需要的信息packageName 即 getData()
、ClassBuilder 即 上面的ViewHolderInfo.Clazz
public class FileBuilder extends AbstractBuilder<String, JavaFile> { ClassBuilder<? extends ClassInfo> mBuilder; public FileBuilder(String packageName, ClassBuilder<? extends ClassInfo> builder) { super(packageName); mBuilder = builder; } // 传入一个 packageName 和 ClassBuilder,完成 build @Override public JavaFile build() { return JavaFile.builder( getData(), mBuilder.build() ).build(); } }
最最后,输出到文件:
public static void generateJavaCode(List<ClassInfo> infoList) { Stream.of(infoList).forEach(it -> { try { // it 指的是解析到的 ViewHolderInfo, // 当然也可以是别的注解的信息,ClassInfo 的子类 new FileBuilder(it.getPackageName(), it.getClassBuilder()) // 传入信息 .build() // 完成配置 .writeTo(sFiler); // 写入文件 }catch (IOException e){ System.out.println(e.getMessage()); } }); }
效果
我们把@ViewHolder
标注在MyAdapter
上,指定布局资源layoutRes
,然后编译。
@ViewHolder(layoutRes = android.R.layout.simple_list_item_1)
private class MyAdapter extends RecyclerView.Adapter<MyAdapter$$ViewHolder> {...}
生成了 MyAdapter$$ViewHolder
!!!
实现了View.inflate()
~~~
package com.fashare.apt_for_viewholder;
import android.content.Context;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.View;
public class MyAdapter$$ViewHolder extends ViewHolder {
public MyAdapter$$ViewHolder(Context context) {
super(View.inflate(context, 17367043, null));
}
}
虽然是编译时生成的,但我们确实可以把它当作自己写的类对待new MyAdapter$$ViewHolder(mContext);
、声明变量MyAdapter$$ViewHolder mHolder;
都没问题。你可能觉得奇怪,然而这个类确实成了和R.class
一样的存在,build前会显示红色,build后才正常。
回到Element和TypeMirror, 被注解元素的获取
最后剩下的是process()
的第一步:
List infoList = getClassInfoList(annotations, roundEnv);
关键点在于roundEnv.getElementsAnnotatedWith()
public interface RoundEnvironment {
Set<? extends Element> getElementsAnnotatedWith(TypeElement a);
Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a);
}
都是获取被注解元素的的信息:
- 可以这样 roundEnv.getElementsAnnotatedWith(mViewHolderTypeElement)
- 也可以这样:roundEnv.getElementsAnnotatedWith(ViewHolder.class);
既然process()
回调给我们的参数是TypeElement
,我们就采取第一种:
private List<ClassInfo> getClassInfoList(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
return Stream.of(annotations)
// 1. 变换: 获取所有被 ViewHolder 标注的对象: Set<Element>
.map(roundEnv:: getElementsAnnotatedWith) // java8 函数引用
// 2. 展平: 去一层嵌套Stream<Set<Element>> 转换为 Stream<Element>
.flatMap(Stream:: of)
// 3. 变换: 转换成 ClassInfo
.map(it -> {
if(it.getAnnotation(ViewHolder.class) != null)
return new ViewHolderInfo((TypeElement) it); // 把被注解的元素信息 it, 传给 ViewHolderInfo()
return null;
})
.filter(it -> it!=null)
// 4. 收集: List<ClassInfo>
.collect(Collectors.toList());
}
我们new
了很多ViewHolderInfo
出来,它们都保存着珍贵的“被注解元素”的信息。它对外提供数据,如我们之前看到的:
getData().getSimpleName()
getData().getPackageName()
getData().getLayoutRes()
it.getClassBuilder()
我们来看 ViewHolderInfo
这个类
public abstract class ClassInfo<E extends Element, A extends Annotation> {
E mAnnotatedElement;
A mAnnotation;
public E getAnnotatedElement() {
return mAnnotatedElement;
}
public A getAnnotation() {
return mAnnotation;
}
...
}
public class ViewHolderInfo extends ClassInfo<TypeElement, ViewHolder>{
public static final String POSTFIX = "$$ViewHolder"; // 实现类的后缀
public ViewHolderInfo(TypeElement typeElement) {
super(typeElement);
}
public ViewHolderInfo(TypeElement element, ViewHolder annotation) {
super(element, annotation);
}
@Override
protected Class<ViewHolder> getAnnotationClazz() {
return ViewHolder.class;
}
@Override
public String getSimpleName() {
return getAnnotatedElement().getSimpleName() + POSTFIX;
}
@Override
public ClassBuilder<? extends ClassInfo> getClassBuilder() {
return new Clazz(this);
}
public int getLayoutRes() {
return getAnnotation().layoutRes();
// 这里取出了我们注解里的值 layout = R.layout.XXX
}
// 之前提到的 Class 级别的配置
private static class Clazz extends ClassBuilder<ViewHolderInfo> { ... }
// 之前提到的 Constructor 的配置
private static class Constructor extends MethodBuilder<ViewHolderInfo> { ... }
总结
看到这里,小伙伴们已经能够自定义注解处理器了吧,看到ButterKnife
也能够脑补其实现方式了吧。
其实,这么demo很大程度上借鉴了这个项目:
https://github.com/sockeqwe/AnnotatedAdapter
该作者还有一些有意思的注解框架:
带参数的Fragment, 依然是注解向呢 https://github.com/sockeqwe/fragmentargs
彩蛋
手贱下载了demo的家伙会发现,咦,这里不止@ViewHolder
这么一个注解啊。还有@Field
、@Adapter
。
@Feild
暴露了啊,其实@ViewHolder
的完全形态是这样滴:
@ViewHolder(
layoutRes = android.R.layout.simple_list_item_1,
fields = {
@Field(idRes = android.R.id.text1, clazz = TextView.class),
}
)
private class MyAdapter extends RecyclerView.Adapter<MyAdapter$$ViewHolder> {
卧槽,牛逼了,原来注解还能嵌套 orz。这个@Feild
是用来生成成员的,如TextView mTextView;
,最后效果是这样滴,生成的文件:
- 集成了
findViewById()
的功能 - 还有数据绑定,
setText()
的功能
package com.fashare.apt_for_viewholder;
import android.content.Context;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.view.View;
import android.widget.TextView;
import java.lang.String;
public class MyAdapter$$ViewHolder extends ViewHolder {
public TextView mTextView_16908308;
public MyAdapter$$ViewHolder(Context context) {
super(View.inflate(context, 17367043, null));
mTextView_16908308 = (TextView)itemView.findViewById(16908308);
}
public void bind(String data) {
mTextView_16908308.setText(data);
}
}
@Adapter
你说,还是好麻烦啊,我连Adapter
都懒得写呢,干脆也自动生成得了。那啥,也满足你:
public class MainActivity extends AppCompatActivity {
@BindView(R.id.rv) RecyclerView mRv;
// 看这个多层嵌套,orz
@Adapter(
viewHolder = @ViewHolder(
layoutRes = android.R.layout.simple_list_item_1,
fields = {
@Field(idRes = android.R.id.text1, clazz = TextView.class),
}
)
)
mAdapter$$Adapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
mRv.setAdapter(mAdapter = new mAdapter$$Adapter(this));
mRv.setLayoutManager(new LinearLayoutManager(this));
mAdapter.setDataList(Arrays.asList("1", "2", "3", "4", "5"));
}
}
最后生成两个文件:
- 一个
MainActivity$$ViewHolder
(和前面的差不多) - 还一个是 Adapter 呢:
mAdapter$$Adapter
为了生成这个花了我不少功夫,不过套路都一样哈。好奇怎么实现的话,自己下Demo看。贴个效果,生成的文件:(未添加范型支持,QAQ)
package com.fashare.apt_for_viewholder;
import android.content.Context;
import android.support.v7.widget.RecyclerView.Adapter;
import android.view.ViewGroup;
import java.lang.Override;
import java.util.ArrayList;
import java.util.List;
public class mAdapter$$Adapter extends Adapter<MainActivity$$ViewHolder> {
Context mContext;
List mDataList;
public mAdapter$$Adapter(Context context) {
mContext = context;
mDataList = new ArrayList();
}
public void setDataList(List dataList) {
mDataList = dataList;
notifyDataSetChanged();
}
@Override
public MainActivity$$ViewHolder onCreateViewHolder(ViewGroup viewgroup, int viewType) {
return new MainActivity$$ViewHolder(mContext);
}
@Override
public void onBindViewHolder(MainActivity$$ViewHolder holder, int position) {
holder.bind((String)mDataList.get(position));
}
@Override
public int getItemCount() {
return mDataList!=null? mDataList.size(): 0;
}
}
呼呼,彩蛋结束了。感想有二:
- 注解嵌套很强大
- 还不如自己写
Adapter
快,2333