从 ButterKnife 到“编译时注解”实战

8 篇文章 0 订阅
2 篇文章 0 订阅

写在前面


下面是一些关于注解的个人认识,可以跳过,直接从知识点部分看起。

自从注解出现以后,很多框架都喜欢用它来干活,显得轻便优雅。我最早邂逅的还是@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,用于实现功能

这里有一个坑要说:为啥一定要分离出一个 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:streamtatarka.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包里,有两个重要的类TypeMirrorElement。它们共同保存了一个代码片段的信息,可以通过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相关接口,而采用TypeMirrorElement作为代替。以获得getSimpleName()之类的东西。


代码生成工具:javapoet

介绍

为啥用它呢,其实你有System.out.println()就完全够了,只需要重定向输出流到某个文件里。当然一些细节会蛋疼死你,有轮子何不用呢?javapoet,嘿,java诗人,用起来还真像写诗呢。

输出一个HelloWorld官方的sample如下(很好理解):
https://github.com/square/javapoet

这里写图片描述

照着例子,代码copyprocess(),把最后的javaFile.writeTo(System.out);改成javaFile.writeTo(mFiler);就完事了。我们成功生成了HelloWorld.java,是不是很开心!!!

你说,妈蛋骗人,我的ViewHolder呢?别急,我们只需把相关信息按如下级别,填充到对应的位置即可:

  • MethodSpec: 函数级别,我们生成一个构造函数试试
  • TypeSepc: 类级别,需要类名、继承关系的信息
  • JavaFile: 包名,”com.example.XXX”

使用

我弄了两个接口BuilderAbstractBuilder,来辅助这些配置:

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
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值