自定义注解框架实现

时间:2021-07-02 20:41:16

 日常项目开发中,注解使用越来越广泛,我们会经常用到各类注解框架为我们减轻工作中的一些重复劳动,比如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的源码。