Android 如何编写基于编译时注解的项目

一、概述

在Android应用开发中,我们常常为了提升开发效率会选择使用一些基于注解的框架,但是由于反射造成一定运行效率的损耗,所以我们会更青睐于编译时注解的框架,例如:

  • butterknife免去我们编写View的初始化以及事件的注入的代码。
  • EventBus3方便我们实现组建间通讯。
  • fragmentargs轻松的为fragment添加参数信息,并提供创建方法。
  • ParcelableGenerator可实现自动将任意对象转换为Parcelable类型,方便对象传输。

类似的库还有非常多,大多这些的库都是为了自动帮我们完成日常编码中需要重复编写的部分(例如:每个Activity中的View都需要初始化,每个实现Parcelable接口的对象都需要编写很多固定写法的代码)。

这里并不是说上述框架就一定没有使用反射了,其实上述其中部分框架内部还是有部分实现是依赖于反射的,但是很少而且一般都做了缓存的处理,所以相对来说,效率影响很小。

但是在使用这类项目的时候,有时候出现错误会难以调试,主要原因还是很多用户并不了解这类框架其内部的原理,所以遇到问题时会消耗大量的时间去排查。

那么,于情于理,在编译时注解框架这么火的时刻,我们有理由去学习:如何编写一个机遇编译时注解的项目

首先,是为了了解其原理,这样在我们使用类似框架遇到问题的时候,能够找到正确的途径去排查问题;其次,我们如果有好的想法,发现某些代码需要重复创建,我们也可以自己来写个框架方便自己日常的编码,提升编码效率;最后也算是自身技术的提升。

注:以下使用IDE为Android Studio.

本文将以编写一个View注入的框架为线索,详细介绍编写此类框架的步骤。

  • 什么是注解
    • 注解分类
    • 注解作用分类
  • 元注解
  • Java内置注解
  • 自定义注解
    • 自定义注解实现及使用
    • 编译时注解
      • 注解处理器
        • 注解处理器基本代码
        • 注解处理器一般处理逻辑
  • 如何编写基于编译时注解的项目
    • 项目结构划分
    • 注解模块的实现
    • 注解处理器的实现
      • Glide遇坑记之分析
        • 收集信息
        • 生成代理类
        • 生成Java代码
    • API模块的实现
  • ButterKnife工作流程解析
    • ButterKnife
      有哪些优势?
    • ButterKnife工作流程
    • Java注解的工作流程
  • 参考资料

APT(Annotation Processing Tool 的简称),可以在代码编译期解析注解,并且生成新的
Java 文件,减少手动的代码输入。现在有很多主流库都用上了 APT,比如
Dagger2, ButterKnife, EventBus3 等,我们要紧跟潮流,与时俱进呐! (ง
•̀_•́)ง

二、编写前的准备

在编写此类框架的时候,一般需要建立多个module,例如本文即将实现的例子:

  • ioc-annotation 用于存放注解等,Java模块
  • ioc-compiler 用于编写注解处理器,Java模块
  • ioc-api 用于给用户提供使用的API,本例为Andriod模块
  • ioc-sample 示例,本例为Andriod模块

那么除了示例以为,一般要建立3个module,module的名字你可以自己考虑,上述给出了一个简单的参考。当然如果条件允许的话,有的开发者喜欢将存放注解和API这两个module合并为一个module。

对于module间的依赖,因为编写注解处理器需要依赖相关注解,所以:

ioc-compiler依赖ioc-annotation

我们在使用的过程中,会用到注解以及相关API

所以ioc-sample依赖ioc-api;ioc-api依赖ioc-annotation

什么是注解

  • 在Java语法中,使用@符号作为开头,并在@后面紧跟注解名。被运用于类,接口,方法和字段之上。

  • 注解也叫元数据,是一种代码级别的说明,与类,接口。枚举是在用一个层次上,他可以声明在包,类,字段,方法,局部变量,方法参数等的前面,用来对这些变量进行说明,注释。注解可以提高代码的可读性,它可以向编译器,虚拟机等解释说明一些事情。降低项目的耦合度,自动生成Java代码,自动完成一些规律性的代码,减少开发者的工作量。

下面通过一个简单的 View 注入项目 ViewFinder 来介绍 APT
相关内容,简单实现了类似于ButterKnife 中的两种注解 @BindView 和 @OnClick 。

三、注解模块的实现

注解模块,主要用于存放一些注解类,本例是模板butterknife实现View注入,所以本例只需要一个注解类:

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

我们设置的保留策略为Class,注解用于Field上。这里我们需要在使用时传入一个id,直接以value的形式进行设置即可。

你在编写的时候,分析自己需要几个注解类,并且正确的设置@Target以及@Retention即可。

注解分类

  • Java内置注解
  • 元注解
  • 自定义注解
    • 运行时注解
    • 编译时注解

项目地址:

四、注解处理器的实现

定义完成注解后,就可以去编写注解处理器了,这块有点复杂,但是也算是有章可循的。

该模块,我们一般会依赖注解模块,以及可以使用一个auto-service

build.gradle的依赖情况如下:

dependencies {
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile project (':ioc-annotation')
}

auto-service库可以帮我们去生成META-INF等信息。

注解作用分类

  • 编写文档
    • 通过代码里标识的元数据生成文档【生成文档doc文档】
  • 代码分析
    • 通过代码里标识的元数据对代码进行分析【使用反射】
  • 编译检查
    • 通过代码里标识的元数据让编译器能够实现基本的编译检查【Override】

Java字段(类成员)和属性

  • 属性只局限于类中方法声明,并不与类中其他的成员相关

  • Java中的属性通常可以理解为get和set方法;而字段通常叫做类成员

  • 字段通常是在类中定义的类成员变量

大概项目结构如下:

(1)基本代码

注解处理器一般继承于AbstractProcessor,刚才我们说有章可循,是因为部分代码的写法基本是固定的,如下:

@AutoService(Processor.class)
public class IocProcessor extends AbstractProcessor{
    private Filer mFileUtils;
    private Elements mElementUtils;
    private Messager mMessager;
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv){
        super.init(processingEnv);
        mFileUtils = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
    }
    @Override
    public Set<String> getSupportedAnnotationTypes(){
        Set<String> annotationTypes = new LinkedHashSet<String>();
        annotationTypes.add(BindView.class.getCanonicalName());
        return annotationTypes;
    }
    @Override
    public SourceVersion getSupportedSourceVersion(){
        return SourceVersion.latestSupported();
    }
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
    }

在实现AbstractProcessor后,process()方法是必须实现的,也是我们编写代码的核心部分,后面会介绍。

我们一般会实现getSupportedAnnotationTypes()getSupportedSourceVersion()两个方法,这两个方法一个返回支持的注解类型,一个返回支持的源码版本,参考上面的代码,写法基本是固定的。

除此以外,我们还会选择复写init()方法,该方法传入一个参数processingEnv,可以帮助我们去初始化一些父类类:

  • Filer mFileUtils; 跟文件相关的辅助类,生成JavaSourceCode.
  • Elements
    mElementUtils;跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
  • Messager mMessager;跟日志相关的辅助类。

这里简单提一下Elemnet,我们简单认识下它的几个子类,根据下面的注释,应该已经有了一个简单认知。

Element 
  - VariableElement //一般代表成员变量
  - ExecutableElement //一般代表类中的方法
  - TypeElement //一般代表代表类
  - PackageElement //一般代表Package

元注解(负责注解其他的注解)

  • @Target

    • 表示该注解用于什么地方,可能的ElementType参数包括:
      • CONSTRUCTOR:构造器的声明
      • FIELD:域声明
      • LOCAL_VARIABLE:局部变量声明
      • METHOD:方法声明
      • PACKAGE:包声明
      • PARAMETER:参数声明
      • TYPE:类,接口或enum声明
  • @Retention

    • 表示在什么级别保留此信息,可选的RetentionPolicy参数包括:
      • SOURCE:注解仅存在代码中,注解会被编译器丢弃
      • CLASS:注解会在class文件中保留,但会被VM丢弃
      • RUNTIME:VM运行期间也会保留该注解,因此可以通过反射来获得该注解
  • @Documented

    • 将注解包含在javadoc中
  • @Inherited

    • 允许子类继承父类的注解
  • viewFinder-annotation – 注解相关模块
  • viewFinder-compiler – 注解处理器模块
  • viewfinder – API 相关模块
  • sample – 示例 Demo 模块

(2)process的实现

process中的实现,相比较会比较复杂一点,一般你可以认为两个大步骤:

  • 收集信息
  • 生成代理类(本文把编译时生成的类叫代理类)

什么叫收集信息呢?就是根据你的注解声明,拿到对应的Element,然后获取到我们所需要的信息,这个信息肯定是为了后面生成JavaFileObject所准备的。

例如本例,我们会针对每一个类生成一个代理类,例如MainActivity我们会生成一个MainActivity$$ViewInjector。那么如果多个类中声明了注解,就对应了多个类,这里就需要:

  • 一个类对象,代表具体某个类的代理类生成的全部信息,本例中为ProxyInfo
  • 一个集合,存放上述类对象(到时候遍历生成代理类),本例中为Map<String, ProxyInfo>,key为类的全路径。

这里的描述有点模糊没关系,一会结合代码就好理解了。

Java内置注解

  • @Override,表示当前的方法定义将覆盖超类中的方法,如果出现错误,编译器就会报错。

    • 当我们的子类覆写父类中的方法的时候,我们使用这个注解,这一定程度的提高了程序的可读性也避免了维护中的一些问题,比如说,当修改父类方法签名(方法名和参数)的时候,你有很多个子类方法签名也必须修改,否则编译器就会报错,当你的类越来越多的时候,那么这个注解确实会帮上你的忙。如果你没有使用这个注解,那么你就很难追踪到这个问题。
  • @Deprecated:如果使用此注解,编译器会出现警告信息。

    • 一个弃用的元素(类,方法和字段)在java中表示不再重要,它表示了该元素将会被取代或者在将来被删除。
      当我们弃用(deprecate)某些元素的时候我们使用这个注解。所以当程序使用该弃用的元素的时候编译器会弹出警告。当然我们也需要在注释中使用@deprecated标签来标示该注解元素。
  • @SuppressWarnings:忽略编译器的警告信息

    • 当我们想让编译器忽略一些警告信息的时候,我们使用这个注解。比如在下面这个示例中,我们的deprecatedMethod()方法被标记了@Deprecated注解,所以编译器会报警告信息,但是我们使用了@SuppressWarnings(“deprecation”)也就让编译器不在报这个警告信息了

实现目标

在通常的 Android
项目中,会写大量的界面,那么就会经常重复地写一些代码,比如:

TextView text = (TextView) findViewById(R.id.tv);
text.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        // on click
    }
});

天天写这么冗长又无脑的代码,还能不能愉快地玩耍啦。所以,我打算通过 ViewFinder 这个项目替代这重复的工作,只需要简单地标注上注解即可。通过控件
id
进行注解,并且@OnClick 可以对多个控件注解同一个方法。就像下面这样子咯:

@BindView(R.id.tv) TextView mTextView;
@OnClick({R.id.tv, R.id.btn})
public void onSomethingClick() {
    // on click
}

a.收集信息

private Map<String, ProxyInfo> mProxyMap = new HashMap<String, ProxyInfo>();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
    mProxyMap.clear();
    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
    //一、收集信息
    for (Element element : elements){
        //检查element类型
        if (!checkAnnotationUseValid(element)){
            return false;
        }
        //field type
        VariableElement variableElement = (VariableElement) element;
        //class type
        TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();//TypeElement
        String qualifiedName = typeElement.getQualifiedName().toString();

        ProxyInfo proxyInfo = mProxyMap.get(qualifiedName);
        if (proxyInfo == null){
            proxyInfo = new ProxyInfo(mElementUtils, typeElement);
            mProxyMap.put(qualifiedName, proxyInfo);
        }
        BindView annotation = variableElement.getAnnotation(BindView.class);
        int id = annotation.value();
        proxyInfo.mInjectElements.put(id, variableElement);
    }
    return true;
}

首先我们调用一下mProxyMap.clear();,因为process可能会多次调用,避免生成重复的代理类,避免生成类的类名已存在异常。

然后,通过roundEnv.getElementsAnnotatedWith拿到我们通过@BindView注解的元素,这里返回值,按照我们的预期应该是VariableElement集合,因为我们用于成员变量上。

接下来for循环我们的元素,首先检查类型是否是VariableElement.

然后拿到对应的类信息TypeElement,继而生成ProxyInfo对象,这里通过一个mProxyMap进行检查,key为qualifiedName即类的全路径,如果没有生成才会去生成一个新的,ProxyInfo与类是一一对应的。

接下来,会将与该类对应的且被@BindView声明的VariableElement加入到ProxyInfo中去,key为我们声明时填写的id,即View的id。

这样就完成了信息的收集,收集完成信息后,应该就可以去生成代理类了。

自定义注解

  • 运行时注解大多数时候实时运行时使用反射来实现所需效果,这很大程度上影响效率
  • 编译时注解在编译时生成对应Java代码实现代码注入

定义注解

创建 module viewFinder-annotation ,类型为 Java
Library,定义项目所需要的注解。

在 ViewFinder 中需要两个注解 @BindView 和 @OnClick 。实现如下:

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

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface OnClick {
    int[] value();
}

@BindView 需要对成员变量进行注解,并且接收一个 int
类型的参数; @OnClick 需要对方法进行注解,接收一组 int
类型参数,相当于给一组 View 指定点击响应事件。

b.生成代理类

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
    //...省略收集信息的代码,以及try,catch相关
    for(String key : mProxyMap.keySet()){
        ProxyInfo proxyInfo = mProxyMap.get(key);
        JavaFileObject sourceFile = mFileUtils.createSourceFile(
                proxyInfo.getProxyClassFullName(), proxyInfo.getTypeElement());
            Writer writer = sourceFile.openWriter();
            writer.write(proxyInfo.generateJavaCode());
            writer.flush();
            writer.close();
    }
    return true;
}

可以看到生成代理类的代码非常的简短,主要就是遍历我们的mProxyMap,然后取得每一个ProxyInfo,最后通过mFileUtils.createSourceFile来创建文件对象,类名为proxyInfo.getProxyClassFullName(),写入的内容为proxyInfo.generateJavaCode().

看来生成Java代码的方法都在ProxyInfo里面。

自定义注解实现及使用

自定义注解使用@interface来声明一个注解。创建一个自定义注解遵循:
public @interface 注解名 {方法参数}

自定义注解示例一

@Documented
@Target(ElementType.METHOD)
@Inherited                                                                                                                                                                                                                                                                                                                                                                           @Retention(RetentionPolicy.RUNTIME)
public @interface Annotation{                                                                                                                                 
    int studentAge() default 18;   //定义默认值
    String studentName();
    String stuAddress();
    String stuStream() default "CSE";
}

@Annotation(studentName = "Chaitanya", stuAddress = "Agra, India")
public class Class {                                                                                                                                                                   
    ...
}

自定义注解示例二

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface getViewTo {
    int value() default  -1;
}

public class MainActivity extends AppCompatActivity {

    @getViewTo(R.id.textview)
    private TextView mTv;

    /**
     * 解析注解,获取控件
     */
    private void getAllAnnotationView() {
        //获得成员变量
        Field[] fields = this.getClass().getDeclaredFields();

        for (Field field : fields) {
          try {
            //判断注解
            if (field.getAnnotations() != null) {
              //确定注解类型
              if (field.isAnnotationPresent(GetViewTo.class)) {
                //允许修改反射属性
                field.setAccessible(true);
                GetViewTo getViewTo = field.getAnnotation(GetViewTo.class);
                //findViewById将注解的id,找到View注入成员变量中
                field.set(this, findViewById(getViewTo.value()));
              }
            }
          } catch (Exception e) {
          }
        }
      }
}

编写 API

创建 module viewfinder,类型为 Android Library。在这个 module
中去定义 API,也就是去确定让别人如何来使用我们这个项目。

首先需要一个 API 主入口,提供静态方法直接调用,就比如这样:

ViewFinder.inject(this);

同时,需要为不同的目标(比如 Activity、Fragment 和 View
等)提供重载的注入方法,最终都调用 inject() 方法。其中有三个参数:

  • host 表示注解 View 变量所在的类,也就是注解类
  • source 表示查找 View 的地方,Activity & View
    自身就可以查找,Fragment 需要在自己的 itemView 中查找
  • provider 是一个接口,定义了不同对象(比如 Activity、View
    等)如何去查找目标 View,项目中分别为 Activity、View
    实现了 Provider 接口。

    public class ViewFinder {

    private static final ActivityProvider PROVIDER_ACTIVITY = new ActivityProvider();
    private static final ViewProvider PROVIDER_VIEW = new ViewProvider();
    
    public static void inject(Activity activity) {
        inject(activity, activity, PROVIDER_ACTIVITY);
    }
    public static void inject(View view) {
        // for view
        inject(view, view);
    }
    public static void inject(Object host, View view) {
        // for fragment
        inject(host, view, PROVIDER_VIEW);
    }
    public static void inject(Object host, Object source, Provider provider) {
        // how to implement ?
    }
    

    }

那么 inject() 方法中都写一些什么呢?

首先我们需要一个接口 Finder,然后为每一个注解类都生成一个对应的内部类并且实现这个接口,然后实现具体的注入逻辑。在 inject() 方法中首先找到调用者对应的 Finder 实现类,然后调用其内部的具体逻辑来达到注入的目的。

接口 Finder 设计如下 :

public interface Finder<T> {
    void inject(T host, Object source, Provider provider);
}

举个例子,为 MainActivity 生成 MainActivity$$Finder,对其注解的 View
进行初始化和设置点击事件,这就跟我们平常所写的重复代码基本相同。

public class MainActivity$$Finder implements Finder<MainActivity> {
    @Override
    public void inject(final MainActivity host, Object source, Provider provider) {
        host.mTextView = (TextView) (provider.findView(source, 2131427414));
        host.mButton = (Button) (provider.findView(source, 2131427413));
        host.mEditText = (EditText) (provider.findView(source, 2131427412));
        View.OnClickListener listener;
        listener = new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                host.onButtonClick();
            }
        };
        provider.findView(source, 2131427413).setOnClickListener(listener);
        listener = new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                host.onTextClick();
            }
        };
        provider.findView(source, 2131427414).setOnClickListener(listener);
    }
}

好了,所有注解类都有了一个名为 xx$$Finder 的内部类。我们首先通过注解类的类名,得到其对应内部类的
Class 对象,然后实例化拿到具体对象,调用注入方法。

public class ViewFinder {

    // same as above

    private static final Map<String, Finder> FINDER_MAP = new HashMap<>();

    public static void inject(Object host, Object source, Provider provider) {
        String className = host.getClass().getName();
        try {
            Finder finder = FINDER_MAP.get(className);
            if (finder == null) {
                Class<?> finderClass = Class.forName(className + "$$Finder");
                finder = (Finder) finderClass.newInstance();
                FINDER_MAP.put(className, finder);
            }
            finder.inject(host, source, provider);
        } catch (Exception e) {
            throw new RuntimeException("Unable to inject for " + className, e);
        }
    }
}

另外代码中使用到了一点反射,所以为了提高效率,避免每次注入的时候都去找 Finder 对象,这里用一个
Map 将第一次找到的对象缓存起来,后面用的时候直接从 Map 里面取。

到此,API
模块的设计基本搞定了,接下来就是去通过注解处理器为每一个注解类生成 Finder内部类。

c.生成Java代码

这里我们主要关注其生成Java代码的方式。

下面主要看生成Java代码的方法:

#ProxyInfo
//key为id,value为对应的成员变量
public Map<Integer, VariableElement> mInjectElements = new HashMap<Integer, VariableElement>();

public String generateJavaCode(){
    StringBuilder builder = new StringBuilder();
    builder.append("package " + mPackageName).append(";nn");
    builder.append("import com.zhy.ioc.*;n");
    builder.append("public class ").append(mProxyClassName).append(" implements " + SUFFIX + "<" + mTypeElement.getQualifiedName() + ">");
    builder.append("n{n");
    generateMethod(builder);
    builder.append("n}n");
    return builder.toString();
}
private void generateMethod(StringBuilder builder){
     builder.append("public void inject("+mTypeElement.getQualifiedName()+" host , Object object )");
    builder.append("n{n");
    for(int id : mInjectElements.keySet()){
        VariableElement variableElement = mInjectElements.get(id);
        String name = variableElement.getSimpleName().toString();
        String type = variableElement.asType().toString() ;

        builder.append(" if(object instanceof android.app.Activity)");
        builder.append("n{n");
        builder.append("host."+name).append(" = ");
        builder.append("("+type+")(((android.app.Activity)object).findViewById("+id+"));");
        builder.append("n}n").append("else").append("n{n");
        builder.append("host."+name).append(" = ");
        builder.append("("+type+")(((android.view.View)object).findViewById("+id+"));");
        builder.append("n}n");
    }
    builder.append("n}n");
}

这里主要就是靠收集到的信息,拼接完成的代理类对象了,看起来会比较头疼,不过我给出一个生成后的代码,对比着看会很多。

package com.zhy.ioc_sample;
import com.zhy.ioc.*;
public class MainActivity$$ViewInjector implements ViewInjector<com.zhy.ioc_sample.MainActivity>{
    @Override
    public void inject(com.zhy.sample.MainActivity host , Object object ){
        if(object instanceof android.app.Activity){
            host.mTv = (android.widget.TextView)(((android.app.Activity)object).findViewById(2131492945));
        }
        else{
            host.mTv = (android.widget.TextView)(((android.view.View)object).findViewById(2131492945));
        }
    }
}

这样对着上面代码看会好很多,其实就死根据收集到的成员变量(通过@BindView声明的),然后根据我们具体要实现的需求去生成java代码。

这里注意下,生成的代码实现了一个接口ViewInjector<T>,该接口是为了统一所有的代理类对象的类型,到时候我们需要强转代理类对象为该接口类型,调用其方法;接口是泛型,主要就是传入实际类对象,例如MainActivity,因为我们在生成代理类中的代码,实际上就是实际类.成员变量的方式进行访问,所以,使用编译时注解的成员变量一般都不允许private修饰符修饰(有的允许,但是需要提供getter,setter访问方法)。

这里采用了完全拼接的方式编写Java代码,你也可以使用一些开源库,来通过Java api的方式来生成代码,例如:javapoet.

A Java API for generating .java source files.

到这里我们就完成了代理类的生成,这里任何的注解处理器的编写方式基本都遵循着收集信息、生成代理类的步骤。

编译时注解

说到编译时注解,就不得不说注解处理器
AbstractProcessor
,如果你有注意,一般第三方注解相关的类库(基于注解的框架),如bufferKnike、ARouter,都有一个Compiler命名的Module,如下图X2.3,这里面一般都是注解处理器,用于编译时处理对应的注解。

注解处理器(Annotation
Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以对自定义注解注册相应的注解处理器,用于处理注解逻辑

javac是收录于JDK中的Java语言编译器。该工具可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于Java虚拟机的字节码。

创建注解处理器

创建 module viewFinder-compiler,类型为 Java
Library,实现一个注解处理器。

这个模块需要添加一些依赖:

compile project(':viewfinder-annotation')
compile 'com.squareup:javapoet:1.7.0'
compile 'com.google.auto.service:auto-service:1.0-rc2'
  • 因为要用到前面定义的注解,当然要依赖 viewFinder-annotation
  • javapoet 是方块公司出的又一个好用到爆炸的裤子,提供了各种 API
    让你用各种姿势去生成 Java 代码文件,避免了徒手拼接字符串的尴尬。
  • auto-service 是 Google
    家的裤子,主要用于注解 Processor,对其生成 META-INF 配置信息。

下面就来创建我们的处理器 ViewFinderProcessor

@AutoService(Processor.class)
public class ViewFinderProcesser extends AbstractProcessor {

    /**
     * 使用 Google 的 auto-service 库可以自动生成 META-INF/services/javax.annotation.processing.Processor 文件
     */

    private Filer mFiler; //文件相关的辅助类
    private Elements mElementUtils; //元素相关的辅助类
    private Messager mMessager; //日志相关的辅助类

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
    }

    /**
     * @return 指定哪些注解应该被注解处理器注册
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }

    /**
     * @return 指定使用的 Java 版本。通常返回 SourceVersion.latestSupported()。
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // to process annotations 
        return false;
    }
}

用 @AutoService 来注解这个处理器,可以自动生成配置信息。

在 init() 可以初始化拿到一些实用的工具类。

在 getSupportedAnnotationTypes() 方法中返回所要处理的注解的集合。

在 getSupportedSourceVersion() 方法中返回 Java 版本。

这几个方法写法基本上都是固定的,重头戏在 process() 方法。

这里插播一下 Element 元素相关概念,后面会用到不少。

Element 元素,源代码中的每一部分都是一个特定的元素类型,分别代表了包、类、方法等等,具体看
Demo。

package com.example;

public class Foo { // TypeElement

    private int a; // VariableElement
    private Foo other; // VariableElement

    public Foo() {} // ExecuteableElement

    public void setA( // ExecuteableElement
            int newA // TypeElement
    ) {
    }
}

这些 Element 元素,相当于 XML 中的 DOM
树,可以通过一个元素去访问它的父元素或者子元素。

element.getEnclosingElement();// 获取父元素
element.getEnclosedElements();// 获取子元素

注解处理器的整个处理过程跟普通的 Java
程序没什么区别,我们可以使用面向对象的思想和设计模式,将相关逻辑封装到
model
中,使得流程更清晰简洁。分别将注解的成员变量、点击方法和整个注解类封装成不同的
model。

public class BindViewField {
    private VariableElement mFieldElement;
    private int mResId;

    public BindViewField(Element element) throws IllegalArgumentException {
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException(
                String.format("Only fields can be annotated with @%s", BindView.class.getSimpleName()));
        }

        mFieldElement = (VariableElement) element;
        BindView bindView = mFieldElement.getAnnotation(BindView.class);
        mResId = bindView.value();
    }
    // some getter methods
}

主要就是在初始化时校验了一下元素类型,然后获取注解的值,在提供几个 get
方法。OnClickMethod 封装类似。

public class AnnotatedClass {

    public TypeElement mClassElement;
    public List<BindViewField> mFields;
    public List<OnClickMethod> mMethods;
    public Elements mElementUtils;

    // omit some easy methods 

    public JavaFile generateFinder() {

        // method inject(final T host, Object source, Provider provider)
        MethodSpec.Builder injectMethodBuilder = MethodSpec.methodBuilder("inject")
            .addModifiers(Modifier.PUBLIC)
            .addAnnotation(Override.class)
            .addParameter(TypeName.get(mClassElement.asType()), "host", Modifier.FINAL)
            .addParameter(TypeName.OBJECT, "source")
            .addParameter(TypeUtil.PROVIDER, "provider");

        for (BindViewField field : mFields) {
            // find views
            injectMethodBuilder.addStatement("host.$N = ($T)(provider.findView(source, $L))", field.getFieldName(),
                ClassName.get(field.getFieldType()), field.getResId());
        }

        if (mMethods.size() > 0) {
            injectMethodBuilder.addStatement("$T listener", TypeUtil.ANDROID_ON_CLICK_LISTENER);
        }
        for (OnClickMethod method : mMethods) {
            // declare OnClickListener anonymous class
            TypeSpec listener = TypeSpec.anonymousClassBuilder("")
                .addSuperinterface(TypeUtil.ANDROID_ON_CLICK_LISTENER)
                .addMethod(MethodSpec.methodBuilder("onClick")
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .returns(TypeName.VOID)
                    .addParameter(TypeUtil.ANDROID_VIEW, "view")
                    .addStatement("host.$N()", method.getMethodName())
                    .build())
                .build();
            injectMethodBuilder.addStatement("listener = $L ", listener);
            for (int id : method.ids) {
                // set listeners
                injectMethodBuilder.addStatement("provider.findView(source, $L).setOnClickListener(listener)", id);
            }
        }
        // generate whole class 
        TypeSpec finderClass = TypeSpec.classBuilder(mClassElement.getSimpleName() + "$$Finder")
            .addModifiers(Modifier.PUBLIC)
            .addSuperinterface(ParameterizedTypeName.get(TypeUtil.FINDER, TypeName.get(mClassElement.asType())))
            .addMethod(injectMethodBuilder.build())
            .build();

        String packageName = mElementUtils.getPackageOf(mClassElement).getQualifiedName().toString();
        // generate file
        return JavaFile.builder(packageName, finderClass).build();
    }
}

AnnotatedClass 表示一个注解类,里面放了两个列表,分别装着注解的成员变量和方法。在generateFinder() 方法中,按照上一节设计的模板,利用 JavaPoet 的
API 生成代码。这部分没啥特别的姿势,照着 JavaPoet
文档 来就好了,文档写得很细致。

有很多地方需要用到对象的类型,普通类型可以用

ClassName get(String packageName, String simpleName, String... simpleNames)

传入包名、类名、内部类名,就可以拿到想要的类型了(可以参考
项目中TypeUtil 类)。

用到泛型的话,可以用

ParameterizedTypeName get(ClassName rawType, TypeName... typeArguments)

传入具体类和泛型类型就好了。

这些 model
都确定好了之后,process() 方法就很清爽啦。使用 RoundEnvironment 参数来查询被特定注解标注的元素,然后解析成具体的
model,最后生成代码输出到文件中。

@AutoService(Processor.class)
public class ViewFinderProcesser extends AbstractProcessor {

    private Map<String, AnnotatedClass> mAnnotatedClassMap = new HashMap<>();

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // process() will be called several times
        mAnnotatedClassMap.clear();

        try {
            processBindView(roundEnv);
            processOnClick(roundEnv);
        } catch (IllegalArgumentException e) {
            error(e.getMessage());
            return true; // stop process
        }

        for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
            try {
                info("Generating file for %s", annotatedClass.getFullClassName());
                annotatedClass.generateFinder().writeTo(mFiler);
            } catch (IOException e) {
                error("Generate file failed, reason: %s", e.getMessage());
                return true;
            }
        }
        return true;
    }

    private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException {
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            BindViewField field = new BindViewField(element);
            annotatedClass.addField(field);
        }
    }

    private void processOnClick(RoundEnvironment roundEnv) {
        // same as processBindView()
    }

    private AnnotatedClass getAnnotatedClass(Element element) {
        TypeElement classElement = (TypeElement) element.getEnclosingElement();
        String fullClassName = classElement.getQualifiedName().toString();
        AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName);
        if (annotatedClass == null) {
            annotatedClass = new AnnotatedClass(classElement, mElementUtils);
            mAnnotatedClassMap.put(fullClassName, annotatedClass);
        }
        return annotatedClass;
    }
}

首先解析注解元素,并放到对应的注解类对象中,最后调用方法生成文件。model
的代码中还会加入一些校验代码,来判断注解元素是否合理,数据是否正常,然后抛出异常,处理器接收到之后可以打印出错误提示,然后直接返回 true 来结束处理。

至此,注解处理器也基本完成了,具体细节参考项目代码。

五、API模块的实现

有了代理类之后,我们一般还会提供API供用户去访问,例如本例的访问入口是

//Activity中
 Ioc.inject(Activity);
 //Fragment中,获取ViewHolder中
 Ioc.inject(this, view);

模仿了butterknife,第一个参数为宿主对象,第二个参数为实际调用findViewById的对象;当然在Actiivty中,两个参数就一样了。

API一般如何编写呢?

其实很简单,只要你了解了其原理,这个API就干两件事:

  • 根据传入的host寻找我们生成的代理类:例如MainActivity->MainActity$$ViewInjector
  • 强转为统一的接口,调用接口提供的方法。

这两件事应该不复杂,第一件事是拼接代理类名,然后反射生成对象,第二件事强转调用。

public class Ioc{
    public static void inject(Activity activity){
        inject(activity , activity);
    }
    public static void inject(Object host , Object root){
        Class<?> clazz = host.getClass();
        String proxyClassFullName = clazz.getName()+"$$ViewInjector";
       //省略try,catch相关代码 
        Class<?> proxyClazz = Class.forName(proxyClassFullName);
        ViewInjector viewInjector = (com.zhy.ioc.ViewInjector) proxyClazz.newInstance();
        viewInjector.inject(host,root);
    }
}
public interface ViewInjector<T>{
    void inject(T t , Object object);
}

代码很简单,拼接代理类的全路径,然后通过newInstance生成实例,然后强转,调用代理类的inject方法。

这里一般情况会对生成的代理类做一下缓存处理,比如使用Map存储下,没有再生成,这里我们就不去做了。

这样我们就完成了一个编译时注解框架的编写。

注解处理器

实现一个自定义注解处理器,至少重写四个方法,并注册你的Processor(为自定义注解注册相应的注解处理器,用于处理注解逻辑)

  • @AutoService(Processor.class),谷歌提供的自动注册注解,为你生成注册Processor所需要的格式文件(com.google.auto相关包)。
  • init(ProcessingEnvironment env),初始化处理器,一般在这里获取我们需要的工具类。
  • getSupportedAnnotationTypes(),指定注解处理器是注册给哪个注解的,返回指定支持的注解类集合。
  • getSupportedSourceVersion() ,指定java版本。
  • process(),处理器实际处理逻辑入口。

实际项目使用

创建 module sample,普通的 Android
module,来演示 ViewFinder 的使用。

在整个项目下的 build.gradle 中添加

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

然后在 sample module 下的 build.gradle 中添加

apply plugin: 'com.neenbedankt.android-apt'

同时添加依赖:

compile project(':viewfinder-annotation')
compile project(':viewfinder')
apt project(':viewfinder-compiler')

然后随便创建个布局,随便添加几个控件,就能体验注解啦。

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.tv) TextView mTextView;
    @BindView(R.id.btn) Button mButton;
    @BindView(R.id.et) EditText mEditText;

    @OnClick(R.id.btn)
    public void onButtonClick() {
        Toast.makeText(this, "onButtonClick", Toast.LENGTH_SHORT).show();
    }

    @OnClick(R.id.tv)
    public void onTextClick() {
        Toast.makeText(this, "onTextClick", Toast.LENGTH_SHORT).show();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ViewFinder.inject(this);
    }
}

这个时候 build
一下项目,就能看到生成的 MainActivity$$Finder 类了,再运行项目就跑起来了。每次改变注解之后,build
一下项目就好啦。

all done ~

这个项目也就是个玩具级的 APT 项目,借此来学习如何编写 APT 项目。感觉 APT
项目更多地是考虑如何去设计架构,类之间如何调用,需要生成什么样的代码,提供怎样的
API 去调用。最后才是利用注解处理器去解析注解,然后用 JavaPoet
去生成具体的代码。

思路比实现更重要,设计比代码更巧妙。

六、总结

本文通过具体的实例来描述了如何编写一个基于编译时注解的项目,主要步骤为:项目结构的划分、注解模块的实现、注解处理器的编写以及对外公布的API模块的编写。通过文本的学习应该能够了解基于编译时注解这类框架运行的原理,以及自己如何去编写这样一类框架。

源码地址: 

注解处理器基本代码

init()方法传入一个参数processingEnv,可以帮助我们去初始化一些辅助类:

  • Filer mFileUtils; 跟文件相关的辅助类,生成JavaSourceCode.
  • Elements
    mElementUtils;跟元素相关的辅助类,帮助我们去获取一些元素相关的信息。
  • Messager mMessager;跟日志相关的辅助类。

参考

  • Annotation-Processing-Tool详解 (大力推荐)
  • Android
    如何编写基于编译时注解的项目
  • JavaPoet 文档
  • ButterKnife (代码结构设计很棒)
注解处理器一般处理逻辑

1、遍历得到源码中,需要解析的元素列表。
2、判断元素是否可见和符合要求。
3、组织数据结构得到输出类参数。
4、输入生成Java文件。
5、错误处理。

Processor处理过程中,会扫描全部Java源码,代码的每一个部分都是一个特定类型(比如类、变量、方法)的Element,它们像是XML一层的层级机构,比如类、变量、方法等,每个Element代表一个静态的、语言级别的构件。

Element代表的是源代码,而TypeElement代表的是源代码中的类型元素,例如类。然而,TypeElement并不包含类本身的信息。你可以从TypeElement中获取类的名字,但是你获取不到类的信息,例如它的父类。这种信息需要通过TypeMirror获取。你可以通过调用elements.asType()获取元素的TypeMirror。

Element 相关子类

  • VariableElement //一般代表成员变量
  • ExecutableElement //一般代表类中的方法
  • TypeElement //一般代表代表类
  • PackageElement //一般代表Package

如何编写基于编译时注解的项目

在Android应用开发中,我们常常为了提升开发效率会选择使用一些基于注解的框架,但是由于反射造成一定运行效率的损耗,所以我们会更青睐于编译时注解的框架,例如:

  • ButterKnife免去我们编写View的初始化以及事件的注入的代码。
  • EventBus3方便我们实现组建间通讯。
  • Fragmentargs轻松的为Fragment添加参数信息,并提供创建方法。
  • ParcelableGenerator可实现自动将任意对象转换为Parcelable类型,方便对象传输。

项目结构划分

在编写此类框架的时候,一般需要建立多个module,例如:

  • xxx-annotation 用于存放注解等,Java模块
  • xxx-compiler 用于编写注解处理器,Java模块
  • xxx-api 用于给用户提供使用的API,本例为Andriod模块
  • xxx-sample 示例,本例为Andriod模块

注解处理器只需要在编译的时候使用,并不需要打包到APK中。因此为了用户考虑,我们需要将注解处理器分离为单独的module。

对于module间的依赖,因为编写注解处理器需要依赖相关注解,所以:
ioc-compiler依赖ioc-annotation>。我们在使用的过程中,会用到注解以及相关API。所以ioc-sample依赖ioc-apiioc-api依赖ioc-annotation

注解模块的实现

注解模块,主要用于存放一些注解类。

注解处理器的实现

实现一个注解处理器,至少需要重写四个方法。该模块,我们一般会依赖注解模块,以及可以使用一个auto-service库,auto-service库可以帮我们去生成META-INF等信息。
build.gradle的依赖情况如下:

dependencies {
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile project (':ioc-annotation')
}

process的实现

process()注解处理器实际处理逻辑入口。主要是获取被注解的参数列表,组织数据结构得到输出类参数,生成Java文件。process中的实现一般可以认为两个步骤:

  • 收集信息
  • 生成代理类(本文把编译时生成的类叫代理类)

什么叫收集信息呢?就是根据你的注解声明,拿到对应的Element,然后获取到我们所需要的信息,这个信息肯定是为了后面生成JavaFileObject所准备的。
例如本例,我们会针对每一个类生成一个代理类,例如MainActivity我们会生成一个MainActivity$$ViewInjector。那么如果多个类中声明了注解,就对应了多个类,这里就需要:

  • 一个类对象,代表具体某个类的代理类生成的全部信息,本例中为ProxyInfo
  • 一个集合,存放上述类对象(到时候遍历生成代理类),本例中Map<String,
    ProxyInfo>,key为类的全路径。
收集信息

首先调用mProxyMap.clear(),因为process可能会多次调用,避免生成重复的代理类,避免生成类的类名已存在异常。

然后,通过roundEnv.getElementsAnnotatedWith()获取被@BindView注解的元素,这里返回值,按照我们的预期应该是VariableElement集合,因为我们用于成员变量上。

接下来for循环我们的元素,首先检查类型是否是VariableElement(对元素列表进行额外判断,校验元素是否可用),然后获取元素VariableElement对应的类信息TypeElement,继而生成ProxyInfo对象。这里先通过一个mProxyMap进行检查,keyqualifiedName即类的全路径,如果没有生成才会去生成一个新的ProxyInfo实例,ProxyInfo与类是一一对应的。

接下来,会将与该类对应的且被@BindView声明的VariableElement加入到ProxyInfo中去,key为我们声明时填写的id,即View的id。
这样就完成了信息的收集,收集完成信息后,应该就可以去生成代理类了。

生成代理类

遍历mProxyMap,然后取得每一个ProxyInfo,最后通过mFileUtils.createSourceFile()来创建文件对象,类名为proxyInfo.getProxyClassFullName(),写入的内容为proxyInfo.generateJavaCode()(生成Java代码)

生成Java代码

ProxyInfo.generateJavaCode()方法通过收集得到的信息,拼接完成的代理类对象。也可以使用开源库,例如:javapoet,来通过Java
API的方式来生成代码。javapoet (com.squareup:javapoet)是一个根据指定参数,生成java文件的开源库。

生成的代码实现了一个接口ViewInjector<T>,该接口是为了统一所有的代理类对象的类型,到时候我们需要强转代理类对象为该接口类型,调用其方法。接口是泛型,主要就是传入实际类对象,例如:MainActivity因为我们在生成代理类中的代码,实际上就是实际类.成员变量的方式进行访问,所以,使用编译时注解的成员变量一般都不允许private修饰符修饰(有的允许,但是需要提供getter,setter访问方法)。

API模块的实现

有了代理类之后,我们一般还会提供API供用户去访问

API一般如何编写呢?

  • 根据传入的host寻找我们生成的代理类:例如:MainActivity->MainActity$$ViewInjector
  • 强转为统一的接口,调用接口提供的方法。

这两件事应该不复杂,第一件事是拼接代理类名,然后反射生成对象,第二件事强转调用。拼接代理类的全路径,然后通过newInstance生成实例,然后强转,调用代理类的inject()方法。

ButterKnife工作流程解析

Butter Knife,专门为Android
View设计的绑定注解,专业解决各种findViewById。

ButterKnife有哪些优势?

  1. 强大的View绑定和Click事件处理功能,简化代码,提升开发效率
  2. 方便的处理Adapter里的ViewHolder绑定问题
  3. 运行时不会影响APP效率,使用配置方便
  4. 代码清晰,可读性强

ButterKnife工作流程

  1. 开始它会扫描Java代码中所有的ButterKnife注解@Bind、@OnClick、@OnItemClicked等。
  2. 当它发现一个类中含有任何一个注解时,
    ButterKnifeProcessor会帮你生成一个Java类,名字<类名>$$ViewInjector.java,这个新生成的类实现了ViewBinder接口。
  3. 这个ViewBinder类中包含了所有对应的代码,比如@Bind注解对应findViewById(),
    @OnClick对应了view.setOnClickListener()等等。
  4. 最后当Activity启动ButterKnife.bind(this)执行时,ButterKnife会去加载对应的ViewBinder类调用它们的bind()方法。

Java注解工作流程

  • 注解是在编译(Compile)时期进行处理的
  • 注解处理器(Annotation
    Processor)读取Java代码处理相应的注解,并且生成对应的代码
  • 生成的Java代码被当做普通的Java类再次编译
  • 注解处理器不能修改存在Java输入文件,也不能对方法做修改或者添加
Java编译流程.png

参考资料

Android注解快速入门和实用解析

Android
如何编写基于编译时注解的项目

自定义Java注解处理器

ButterKnife框架原理

发表评论

电子邮件地址不会被公开。 必填项已用*标注