一个Demo带你了解编译时注解

概要

之前在项目中使用到Dagger2,在查看源码的过程中产生了一些疑惑。为什么我使用@Component , @Module , @Inject注解就可以实现依赖注入呢?于是我带着这个疑惑开始学习注解相关的一些知识,希望能通过一个Demo来了解它的原理。

demo地址:https://github.com/chrissen0814/AnnotationDemo

本文的目的是探究编译时注解的实现原理,对于注解的定义以及语法相关的基础知识还要请读者们自行学习,本文并不会涉及到这些知识点。

demo实现

在demo里我们实现了@BindView的注解功能(类似于ButterKnife的@BindView),用于实现findViewById()的功能。

先来看一下我们实现的效果

    public class MainActivity extends AppCompatActivity {

        @BindView(R.id.text_view)
        TextView mTextView;
        @BindView(R.id.button)
        Button mButton;

        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            Butter.bind(this);
            mTextView.setText("Hello World and Thank You");
            mButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mTextView.setText("Text has changed.");
                }
            });
        }
    }

和ButterKnife的@BindView()注解很相似,只是在绑定的时候我们是用Butter.bind()来代替ButterKnife.bind()。

我们再来看一下项目结构:

  • app实现的demo入口
  • apt-annotation包里主要用来定义注解
  • apt-api主要提供对外的API接口(类似于ButterKnife.bind())
  • apt-compiler是我们关键类,主要用来实现@BindView注解的功能

apt-annotation

这个包里面我们定义了一个BindView的注解。

apt-compiler

这个包里面包含两个文件,一个是VariableInfo,里面有两个属性:mViewId view的id , VariableElement VariableElement标示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数 ; 另外一个是继承子AbstractProcessor的ViewInjectProcessor , 这个类是我们的核心类,在这个类里面我们要实现注解@BindView()的功能。具体的逻辑在下文会细说。

apt-api

这个包里面只有一个类Butter , 也就是我们在Activity中调用的Butter.bind()所在的包,这个类里面只有一个方法:bind()。

通过上面的讲解你应该了解到,如果我们想实现一个注解,我们需要三步:第一步是定义注解,第二步是实现注解功能,第三步是提供API。

那接下来我们就分别来讲解一下这三步以及具体的逻辑。

实现与分析

apt-annotation

首先我们需要新建一个module叫apt-annotation,这个module的作用是定义我们所需要的注解。需要注意的是这个module的等级是java-library,为什么?因为我们所定义的注解只需要java等级就可以了。

新建文件名为BindView的注解,选择类型的时候注意选择@interface。

//保留策略(SOURCE,CLASS,RUNTIME)
@Retention(RetentionPolicy.CLASS)
//作用域
@Target(ElementType.FIELD)
public @interface BindView {
    //注解中的值为int类型
    int value();
}

apt-compiler

这个是我们的核心module,我们要实现的BindView的功能需要在这个module里实现。

新建module,类型是java-library。这个包里只有两个类:VariableInfo:变量信息,主要是把view的id和VariableElement进行绑定。VariableElement里面提供了很多和变量相关的信息,这个在实现BindView的功能时是需要用到的。另外一个是ViewInjectProcessor,看名字也应该知道这个类的作用是View注解处理器。

//不要写成Process.class
@AutoService(Processor.class)
//设置支持的注解类型(也可以通过重写方法实现)
@SupportedAnnotationTypes({"com.chrissen.apt_annotation.BindView"})
//设置支持(也可以通过重写方法实现)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class ViewInjectProcessor extends AbstractProcessor {

    private Map<String,List<VariableInfo>> classMap = new HashMap<>();
    private Map<String,TypeElement> classTypeElement = new HashMap<>();
    //工具类,用于获取Element信息
    private Elements mUtils;
    //生成java文件的类(生成代理工具类)
    private Filer filer;


    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
        mUtils = processingEnvironment.getElementUtils();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {

        //收集所需信息
        collectInfo(roundEnvironment);

        //生成相应的代理类代码
        writeToFile();

        return true;
    }

    private void writeToFile() {
        try {
            for (String classFullName : classMap.keySet()) {
                TypeElement typeElement = classTypeElement.get(classFullName);

                MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
                        .addModifiers(Modifier.PUBLIC)
                        .addParameter(ParameterSpec.builder(TypeName.get(typeElement.asType()), "activity").build());
                List<VariableInfo> variableList = classMap.get(classFullName);
                for (VariableInfo variableInfo : variableList) {
                    VariableElement variableElement = variableInfo.getVariableElement();

                    String variableName = variableElement.getSimpleName().toString();

                    String variableFullName = variableElement.asType().toString();

                    constructor.addStatement("activity.$L=($L)activity.findViewById($L)", variableName, variableFullName, variableInfo.getViewId());
                }


                TypeSpec typeSpec = TypeSpec.classBuilder(typeElement.getSimpleName() + "$$ViewInjector")
                        .addModifiers(Modifier.PUBLIC)
                        .addMethod(constructor.build())
                        .build();

                //通过mUtils获取完整的包名
                String packageFullName = mUtils.getPackageOf(typeElement).getQualifiedName().toString();
                JavaFile javaFile = JavaFile.builder(packageFullName, typeSpec)
                        .build();

                javaFile.writeTo(filer);
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    private void collectInfo(RoundEnvironment roundEnvironment) {
        classMap.clear();
        classTypeElement.clear();
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        for(Element element : elements){
            int viewId = element.getAnnotation(BindView.class).value();
            VariableElement variableElement = (VariableElement) element;
            TypeElement typeElement = (TypeElement) variableElement.getEnclosingElement();
            String classFullName = typeElement.getQualifiedName().toString();

            List<VariableInfo> variableInfoList = classMap.get(classFullName);
            if(variableInfoList == null){
                variableInfoList = new ArrayList<>();
                classMap.put(classFullName,variableInfoList);
                classTypeElement.put(classFullName,typeElement);
            }
            VariableInfo variableInfo = new VariableInfo();
            variableInfo.setVariableElement(variableElement);
            variableInfo.setViewId(viewId);
            variableInfoList.add(variableInfo);
        }
    }

//    可以通过重写方法来指定支持的SourceVersion
//    @Override
//    public SourceVersion getSupportedSourceVersion() {
//        return SourceVersion.RELEASE_7;
//    }

//    通过重写方法来设置支持的注解类型
//    @Override
//    public Set<String> getSupportedAnnotationTypes() {
//        Set<String> set = new HashSet();
//        set.add("com.chrissen.apt_annotation.BindView");
//        return set;
//    }


}

需要继承自AbstractProcessor,这个类的作用就是用来处理注解的。

在这个类上需要设置几个注解,具体的作用注释里已经写的非常清楚了,其中@SupportedAnnotationTypes和@SupportedSourceVersion也可以通过重写方法来实现,代码中也已经写出来了。我自己在实现的过程中犯了个小错误:在定义@AutoService()时错误的写成了Process.class导致报错异常,正确的应该是Processor.class。

在处理注解的时候主要分为两步:第一步是收集信息,第二步是生成相对应的代码。

这个部分的代码很多都是模板,不同的地方在于生成代码的时候是根据你注解所需要实现的功能。

apt-api

新建module,类型为android.library,原因是这个类里我们需要Activity这个类,所以不能再用java-library。

这个类里只有一个Butter类,里面提供一个bind()方法。

public class Butter {

    public static void bind(Activity host){
        String classFullName = host.getClass().getName() + "$$ViewInjector";
        try {
            Class proxy = Class.forName(classFullName);
            Constructor constructor = proxy.getConstructor(host.getClass());
            //newInstance()返回Object
            constructor.newInstance(host);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

总结

通过自己实现一个注解,你应该大概了解了注解的工作机制:在AbstractProcessor的process()方法里处理,主要分为两步:第一步是收集和注解相关的信息,第二步生成对应的代码。

当然我们只要写了一个事例,所以会有细节的地方没有考虑到,我们的重点主要是了解编译时注解的工作原理。

参考文章

猜你喜欢

转载自blog.csdn.net/chrissen/article/details/80825548