自定义注解框架实现

来源:互联网 发布:美女适合当程序员吗 编辑:程序博客网 时间:2024/06/05 09:51

 日常项目开发中,注解使用越来越广泛,我们会经常用到各类注解框架为我们减轻工作中的一些重复劳动,比如AndroidAnnotation、Dagger2、ButterKnife等这些大名鼎鼎的框架。这些框架有一个共同点就是使用编译时生成代码代替反射,大大优化了性能。
 那为什么一个小小的注解@BindView就可以实现view的查找功能呢?本文就来一探究竟,打造一个类似ButterKnife简单实现view绑定的注解框架。
 在开始之前,我们先认识一项关键技术APT(Annotation Processing Tool)。官方解释:APT是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,根据注解自动生成代码。简单的说就是APT在编译时把注解生成代码,关于APT更多知识此处不展开,有兴趣可以百度查看相关资料。
 伴随着去年Android Gradle 插件 2.2版本发布,android-apt的作者在官网发表声明后续将不会继续维护android-apt,并推荐大家使用 Android 官方插件annotationProcessor。不过由于很多框架还是用的APT,本文还是基于APT实现。

一、创建工程

 首先在Android studio里新建一个Android工程APTDemo。为了使用android-apt插件,需要在工程的build.gradle中加入依赖。

classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

二、创建 annotation Module

 新建一个Java Library Module,命名annotation,然后创建一个BindView注解类。

@Target(ElementType.FIELD)@Retention(RetentionPolicy.CLASS) public @interface BindView {    int value() default 0;}

 BindView的target是FIELD,只对成员变量进行注解,有一个int类型的参数。默认值为0,用来传入view的Id。
 build.gradle,采用默认就好。

apply plugin: 'java'dependencies {    compile fileTree(dir: 'libs', include: ['*.jar'])}sourceCompatibility = "1.7"targetCompatibility = "1.7"

三、创建 compiler Module

 新建一个Java Library Module,命名compiler。
 build.gradle

apply plugin: 'java'dependencies {    compile fileTree(dir: 'libs', include: ['*.jar'])    compile 'com.google.auto.service:auto-service:1.0-rc2'    compile 'com.squareup:javapoet:1.7.0'    compile project(':annotation')}sourceCompatibility = "1.7"targetCompatibility = "1.7"

 依赖了annotation模块,因为要使用annotation中定义的Bindview注解,另外引入了auto-service和javapoet库。auto-service ,主要用于注解 Processor,对其生成 META-INF 配置信息。javapoet可以通过预先设置好的规则,自动生成Java 代码文件,这个真是个好东西。
 新建BindViewProcesor类,这个类就是整个注解框架的核心,包括自动生成代码等。

@AutoService(Processor.class)public class BindViewProcesor extends AbstractProcessor {    @Override    public Set<String> getSupportedAnnotationTypes() {        return super.getSupportedAnnotationTypes();    }    @Override    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {        return false;    }}

 下面我们来编辑BindViewProcesor类,主要逻辑都在process方法中。
 为了一些工具类的方便使用,重写父类的init方法。

 private Elements elements; private Filer filer; @Override public synchronized void init(ProcessingEnvironment processingEnvironment) {     super.init(processingEnvironment);     elements = processingEnvironment.getElementUtils();     filer = processingEnvironment.getFiler(); }

 Elements是元素操作相关的辅助类,主要用于获取各种元素,结构类似DOM树。Filer是文件操作的辅助类。parentAndChildMap是一个map,用来存放类与方法的对应关系。
 修改getSupportedAnnotationTypes,指定可以被注解处理器处理的类型,这里是BindView.class。

@Override    public Set<String> getSupportedAnnotationTypes() {        return Collections.singleton(BindView.class.getCanonicalName());    }

 另外还有一个指定java版本的方法getSupportedSourceVersion,这里我们使用注解@SupportedSourceVersion(SourceVersion.RELEASE_7)。
 下面就是核心方法process的修改,大致的步骤如下:
  1、获取所有标注了@BindView注解的的Element。
  2、遍历标注了注解的Element集合,获取每一个Element的父元素,由于@BindView的Target是FIELD,那么父元素就是该FIELD的类即TypeElement。当然由于一个类中可能有多个标记了@BindView的字段,此处用HashMap来存放之间的对应关系。
  3、遍历HashMap,通过javapoet生成目标类。先指定MethodSpec的生成规则,接着指定TypeSpec和JavaFile的规则,最后调用javaFile.writeTo(filer)生成Java文件。

 for (Map.Entry<TypeElement, List<Element>> entry : parentAndChildMap.entrySet()) {            TypeElement typeElement = entry.getKey();            MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bindView").addModifiers(Modifier.PUBLIC,                    Modifier.STATIC).returns(void.class).addParameter(ClassName.get(typeElement.asType()), "activity");            List<Element> childElementList = entry.getValue();            for (Element element : childElementList) {                int id = element.getAnnotation(BindView.class).value();                String statement = String.format("activity.%s = (%s)activity.findViewById(%d)", element.getSimpleName                        (), ClassName.get(element.asType()).toString(), id);                methodSpecBuilder.addStatement(statement);            }            TypeSpec typeSpec = TypeSpec.classBuilder("BindView$$" + typeElement.getSimpleName()).addModifiers                    (Modifier.PUBLIC, Modifier.FINAL).superclass(ClassName.get(typeElement.asType())).addMethod                    (methodSpecBuilder.build()).build();            JavaFile javaFile = JavaFile.builder(elements.getPackageOf(typeElement).getQualifiedName().toString(),                     typeSpec).build();            try {                javaFile.writeTo(filer);            } catch (Exception e) {                e.printStackTrace();            }        }

 具体javapoet的使用方法,可以参见Javapoet源码。后面有时间再单独写一篇关于javapoet的。
 完整的代码如下:

@AutoService(Processor.class)@SupportedSourceVersion(SourceVersion.RELEASE_7)public class BindViewProcesor extends AbstractProcessor {    private Elements elements;    private Filer filer;    private HashMap<TypeElement,List<Element>> parentAndChildMap;    @Override    public synchronized void init(ProcessingEnvironment processingEnvironment) {        super.init(processingEnvironment);        elements = processingEnvironment.getElementUtils();        filer = processingEnvironment.getFiler();    }    @Override    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {        Set<? extends Element> fieldElements = roundEnv.getElementsAnnotatedWith(BindView.class);        if (fieldElements == null) {            return false;        }        parentAndChildMap = new LinkedHashMap<>();        for (Element fieldEle : fieldElements) {            TypeElement typeElement = (TypeElement) fieldEle.getEnclosingElement();            if (parentAndChildMap.containsKey(typeElement)) {                parentAndChildMap.get(typeElement).add(fieldEle);            } else {                List<Element> childEleList = new ArrayList<>();                childEleList.add(fieldEle);                parentAndChildMap.put(typeElement, childEleList);            }        }        for(Map.Entry<TypeElement,List<Element>> entry:parentAndChildMap.entrySet()){            TypeElement typeElement = entry.getKey();            MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bindView")                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)                    .returns(void.class)                    .addParameter(ClassName.get(typeElement.asType()), "activity");            List<Element> childElementList = entry.getValue();            for(Element element:childElementList){                int id = element.getAnnotation(BindView.class).value();                String statement = String.format("activity.%s = (%s)activity.findViewById(%d)", element                        .getSimpleName(), ClassName.get(element.asType()).toString(), id);                methodSpecBuilder.addStatement(statement);            }            TypeSpec typeSpec = TypeSpec.classBuilder("BindView$$" + typeElement.getSimpleName()).addModifiers(Modifier                    .PUBLIC, Modifier.FINAL).superclass(ClassName.get(typeElement.asType())).addMethod(methodSpecBuilder.build())                    .build();            JavaFile javaFile = JavaFile.builder(elements.getPackageOf(typeElement).getQualifiedName().toString(),                    typeSpec).build();            try {                javaFile.writeTo(filer);            } catch (Exception e) {                e.printStackTrace();            }        }        return true;    }    @Override    public Set<String> getSupportedAnnotationTypes() {        return Collections.singleton(BindView.class.getCanonicalName());    }}

四、使用注解

 为了在app模块中使用注解,需要在app的build.gradle配置注解的依赖。

compile project(':compiler')

 新建一个MainActivity,在textview上标记注解@BindView(R.id.text)

public class MainActivity extends AppCompatActivity {    @BindView(R.id.text)    TextView textView;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);    }}

 然后编译一下整个工程,编译完成之后,会在app/build/generated/source/apt/debug/目录下自动生成BindView$$MainActivity类。

public final class BindView$$MainActivity extends MainActivity {  public static void bindView(MainActivity activity) {    activity.textView = (android.widget.TextView)activity.findViewById(2131427415);  }}

 这个就是compiler中根据注解按照设定的规则使用Javapoet自动生成的。如果不知道在compiler中怎样重写process方法,我们可以先手动写出BindView$$MainActivity类,然后参考这个类再去想我们该怎样写自动生成代码的规则,这样会简单很多。
 此时MainActivity中textview还没有初始化,需要在onCreate方法中调用BindView$$MainActivity.bindView(this)进行注册。

 @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        BindView$$MainActivity.bindView(this);        textView.setText("Hello World!"); }

 此时就完成了通过一个@BindView注解实现view的初始化的目的,整个流程都是套路,具体实现就是compiler中的process方法了。当然本文中的自定义框架只是初步的实现了@BindView的功能,如果想要完整的实现类似ButterKnife的功能,可以参考ButterKnife的源码。

原创粉丝点击