网易云微专业安卓-APT黄金搭档JavaPoet,让框架更稳定更简洁

文章纯属个人学习的代码实现

网易云微专业公开课这节课 用JavaPoet简单实现了ButterKnifeBindView功能

原理其实就是通过注解处理器处理注解,然后利用 JavaPoet面向对象的语法动态生成了一个Java文件

这里加入我们希望完成MainActivityBindView功能,MainActivity的代码如下

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    TextView tv;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        tv.setText("Hello android");
    }

}

我们要看到两个需要注意的地方,第一个就是

    @BindView(R.id.tv)
    TextView tv;

这里就是我们要绑定的视图
第二个地方是这里

  ButterKnife.bind(this);

我们要用注解处理器和JavaPoet生成这样的文件

public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
  @Override
  public void bind(MainActivity target) {
    target.tv = target.findViewById(2131165359);
  }
}

这里可以看到我们生成了一个以MainActivity+$$ViewBinder的一个 java文件,他需要实现一个接口ViewBinder<MainActivity>
我们可以看看ViewBinder<MainActivity>这个接口

public interface ViewBinder<T> {
    void bind(T target);
}

这里声明了一个泛型接口,表示他 接受任何泛型对象,不同泛型对象实现不同的bind(T target)
方法,比如我们刚刚看到的MainActivity$$ViewBinder类实现的

 @Override
  public void bind(MainActivity target) {
    target.tv = target.findViewById(2131165359);
  }

这里传入的类型是MainActivity
然后我们可以看到这个文件其实就是帮我们做了一个操作,利用传进来的MainActivity完成了tv控件的绑定 ,记住了这里的代码不用自己写,是注解处理器和JavaPoet生成的。

但是看到这里有同学要问了,你生成了这个文件,但是我没有调用呀?嗯,问的很好,这里我们还记得

ButterKnife.bind(this);

吗,我们来看看他的代码

public class ButterKnife {
    //target.tv=target.findViewById(R.id.tv)
    public static void bind(Object object) {
     String  className=object.getClass().getName()+"$$ViewBinder";

     //类加载
        try {
            Class<?> clazz = Class.forName(className);
            ViewBinder viewBinder = (ViewBinder) clazz.newInstance();
            viewBinder.bind(object);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

其实这里是通过类加载就是这里的Class.forName实现了java文件的查找,找到了之后我们就可以操作这个Class对象,我们这里直接调用Class对象的实例化方法newInstance()去新建一个对象,这个对象是什么呢?
很简单,我们看到这里

 String  className=object.getClass().getName()+"$$ViewBinder";

这是我们上面看到的那个类呀MainActivity$$ViewBinder,那我们应该记得它实现了ViewBinder接口,那接口的实现类就可以转成对应的接口对象,这个有点拗口,不对想想也应该能理解,我们安卓里面很多类似的操作,比如把点击事件的接口返回一个接口对象,比如下面这样

 tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        });

这个匿名内部类不就完成了一个接口实现类,不同的点击事件就是不同的接口实现类,结果都返回一个View.OnClickListener的接口对象

好回到我们刚刚那里,

viewBinder.bind(object);

最后我们调用了这个,啥意思?其实就相当于

 MainActivity$$ViewBinder.bind(mainActivity)

这里是不是就调用了我们之前用注解处理器和JavaPoet生成的java文件,所以这里也很清楚的可以知道ButterKinfe为啥如果我们不调用这个方法,页面视图绑定会空指针。

上面基本把原理讲完,最后我们看看注解处理器和JavaPoet怎么生成我们需要的这个文件

public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
  @Override
  public void bind(MainActivity target) {
    target.tv = target.findViewById(2131165359);
  }
}

我先扔一堆代码给大家,但是不要慌,后面我会一个个分析

@AutoService(Processor.class)
@SupportedAnnotationTypes({Constants.BINDERVIEW_ANNOTATION_TYPES})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class BindViewProcessor extends AbstractProcessor {
    private Elements elementUitls;
    private Types typeUtils;
    private Messager messager;
    private Filer filer;

    private Map<TypeElement, List<Element>> tempBindViewMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUitls = processingEnv.getElementUtils();
        typeUtils = processingEnv.getTypeUtils();
        messager = processingEnv.getMessager();
        filer = processingEnv.getFiler();
        messager.printMessage(Diagnostic.Kind.NOTE, "init finish,start process annotation ..........");
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        if (set.isEmpty()) return false;
        Set<? extends Element> bindViewElements = roundEnv.getElementsAnnotatedWith(BindView.class);
        if (!bindViewElements.isEmpty()) {
            valueOfMap(bindViewElements);
            try {
                createJavaFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return true;
        }
        return false;
    }

    private void createJavaFile() throws IOException {
        if (tempBindViewMap.isEmpty()) return;


        for (Map.Entry<TypeElement, List<Element>> entry : tempBindViewMap.entrySet()) {
            //获取当前类名
            ClassName className = ClassName.get(entry.getKey());

            //生成一个名为target参数 类型就是上面的typeName
            ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(entry.getKey()), Constants.TARGET_PARAMETER_NAME)
                    .build();//target

            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.BIND_METHOD_NAME)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(parameterSpec);

            for (Element fieldElement : entry.getValue()) {
                String fieldName = fieldElement.getSimpleName().toString();
                //获取BindView上面的id值
                int viewId = fieldElement.getAnnotation(BindView.class).value();
                String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
                //拼接成target.tv=target.findViewById(R.id.tv)
                methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
                        Constants.TARGET_PARAMETER_NAME, viewId);
            }

            //找得到所有实现了ViewBinder接口的类
            TypeElement viewBindType = elementUitls.getTypeElement(Constants.VIEWBINDER);
            // 这里需要理解一下 实现接口泛型 ViewBinder<MainActivity> 拼接成这个样子
            ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType),
                    className);
            if(typeName!=null) {
                JavaFile.builder(className.packageName(),
                        TypeSpec.classBuilder(className.simpleName() + "$$ViewBinder")
                                .addSuperinterface(typeName)  //实现viewBinder接口
                                .addModifiers(Modifier.PUBLIC)
                                .addMethod(methodBuilder.build())
                                .build()

                ).build().writeTo(filer);
            }

        }

    }

    private void valueOfMap(Set<? extends Element> bindViewElements) {
        for (Element element : bindViewElements) {
            //注解在属性之上,属性节点父节点是类节点
            TypeElement classElement = (TypeElement) element.getEnclosingElement();
            if (tempBindViewMap.containsKey(classElement)) {
                tempBindViewMap.get(classElement).add(element);
            } else {
                List<Element> fields = new ArrayList<>();
                fields.add(element);
                tempBindViewMap.put(classElement, fields);
            }

        }
    }
}

首先我们看看这个类头部的注解

@AutoService(Processor.class)
@SupportedAnnotationTypes({Constants.BINDERVIEW_ANNOTATION_TYPES})
@SupportedSourceVersion(SourceVersion.RELEASE_7)

这里@AutoService(Processor.class)表示这是一个注解处理器
@SupportedAnnotationTypes({Constants.BINDERVIEW_ANNOTATION_TYPES})表示这个注解处理器需要处理的注解类型,这里这个名字必须正确,比如我这里的是

public static final String BINDERVIEW_ANNOTATION_TYPES="com.example.annotation.BindView";

错了就出问题了,如果发现注解处理器不生效首先检查一下这里

@SupportedSourceVersion(SourceVersion.RELEASE_7)

然后这里就是支持JDK7
我们再看看下面这个几个部分

private Elements elementUitls;
    private Messager messager;
    private Filer filer;

    private Map<TypeElement, List<Element>> tempBindViewMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUitls = processingEnv.getElementUtils();
        messager = processingEnv.getMessager();
        filer = processingEnv.getFiler();
        messager.printMessage(Diagnostic.Kind.NOTE, "init finish,start process annotation ..........");
    }

首先是这个Elements elementUitls是用来操作类元素的,具体作用我们等一下分析下面的代码可以看到
Messager messager用于控制台打印消息的,Filer filer用于输出Java文件的,Map<TypeElement, List<Element>> tempBindViewMap主要用来存储类里面的元素信息,啥意思?就是用来存储类似

 @BindView(R.id.tv)
    TextView tv;

这样子的类变量信息,后面会分析到

init方法基本就是初始化这几个变量

下面就是核心方法,注解处理了

 @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
        if (set.isEmpty()) return false;
        Set<? extends Element> bindViewElements = roundEnv.getElementsAnnotatedWith(BindView.class);
        if (!bindViewElements.isEmpty()) {
            valueOfMap(bindViewElements);
            try {
                createJavaFile();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return true;
        }
        return false;
    }

我们通过

   Set<? extends Element> bindViewElements = roundEnv.getElementsAnnotatedWith(BindView.class);

找到所有包含BindView注解的对象,然后我们看看valueOfMap做了啥

private void valueOfMap(Set<? extends Element> bindViewElements) {
        for (Element element : bindViewElements) {
            //注解在属性之上,属性节点父节点是类节点
            TypeElement classElement = (TypeElement) element.getEnclosingElement();
            if (tempBindViewMap.containsKey(classElement)) {
                tempBindViewMap.get(classElement).add(element);
            } else {
                List<Element> fields = new ArrayList<>();
                fields.add(element);
                tempBindViewMap.put(classElement, fields);
            }

        }
    }

这个方法其实就是通过遍历所有的添加了BindView注解的对象,然后

  TypeElement classElement = (TypeElement) element.getEnclosingElement();

找到对应的属于哪个类的属性,这个 classElement就是对应的类名,我们这里其实就是MainActivity类,然后判断Map里面是否有该类,如果存在,就取出来然后给他的List<Element>对象添加一个element即使一个属性对象,然后不存在,就新建一个List<Element>对象并且添加一个element然后把对应的类和List<Element>放入Map存起来

说的很复杂哈,其实就是遍历你类里面使用到BindView注解的属性,然后完事

下面到了重头戏createJavaFile()这个方法即使真正生成Java文件的地方

  private void createJavaFile() throws IOException {
        if (tempBindViewMap.isEmpty()) return;
        
        for (Map.Entry<TypeElement, List<Element>> entry : tempBindViewMap.entrySet()) {
            //获取当前类名
            ClassName className = ClassName.get(entry.getKey());

            //生成一个名为target参数 类型就是上面的typeName
            ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(entry.getKey()), Constants.TARGET_PARAMETER_NAME)
                    .build();//target

            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.BIND_METHOD_NAME)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(parameterSpec);

            for (Element fieldElement : entry.getValue()) {
                String fieldName = fieldElement.getSimpleName().toString();
                //获取BindView上面的id值
                int viewId = fieldElement.getAnnotation(BindView.class).value();
                String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
                //拼接成target.tv=target.findViewById(R.id.tv)
                methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
                        Constants.TARGET_PARAMETER_NAME, viewId);
            }

            //找得到所有实现了ViewBinder接口的类
            TypeElement viewBindType = elementUitls.getTypeElement(Constants.VIEWBINDER);
            // 这里需要理解一下 实现接口泛型 ViewBinder<MainActivity> 拼接成这个样子
            ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType),
                    className);
            if(typeName!=null) {
                JavaFile.builder(className.packageName(),
                        TypeSpec.classBuilder(className.simpleName() + "$$ViewBinder")
                                .addSuperinterface(typeName)  //实现viewBinder接口
                                .addModifiers(Modifier.PUBLIC)
                                .addMethod(methodBuilder.build())
                                .build()

                ).build().writeTo(filer);
            }

        }

    }

这个方法很长,没关系,我们从下往上看

//找得到所有实现了ViewBinder接口的类
            TypeElement viewBindType = elementUitls.getTypeElement(Constants.VIEWBINDER);
            // 这里需要理解一下 实现接口泛型 ViewBinder<MainActivity> 拼接成这个样子
            ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType),
                    className);
            if(typeName!=null) {
                JavaFile.builder(className.packageName(),
                        TypeSpec.classBuilder(className.simpleName() + "$$ViewBinder")
                                .addSuperinterface(typeName)  //实现viewBinder接口
                                .addModifiers(Modifier.PUBLIC)
                                .addMethod(methodBuilder.build())
                                .build()

                ).build().writeTo(filer);
            }

这里啥意思,首先这两句代码

TypeElement viewBindType = elementUitls.getTypeElement(Constants.VIEWBINDER);
            // 这里需要理解一下 实现接口泛型 ViewBinder<MainActivity> 拼接成这个样子
            ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType),
                    className);

我们通过开头讲到的Element elementUitls这个可以操作类的对象,找到一个接口类型Constants.VIEWBINDER=public static final CharSequence VIEWBINDER = "com.example.library.ViewBinder";这里也不能错,错了玩完
找到这个接口之后,通过ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBindType), className);构建成一个类似这样的接口ViewBinder<MainActivity>
这里的className在开头的这个位置我们能看到

//获取当前类名
            ClassName className = ClassName.get(entry.getKey());

作用就是获取当前类名,这是一个对象哈,不单单是类名还有其他属性
然后我们看这里

 JavaFile.builder(className.packageName(),
                        TypeSpec.classBuilder(className.simpleName() + "$$ViewBinder")

通过className获取包名,然后传入类型对应要生成的类名这里其实就是MainActivity$$ViewBinder

然后

.addSuperinterface(typeName)  //实现viewBinder接口

实现ViewBinder<MainActivity>接口
然后

 .addModifiers(Modifier.PUBLIC)

声明类的访问权限为public
然后

  .addMethod(methodBuilder.build())

传入要实现的方法,这个我们等一下往上看能看到,可以先预告一下,我们还记得这个类吗

public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
  @Override
  public void bind(MainActivity target) {
    target.tv = target.findViewById(2131165359);
  }
}

我们上面基本完成了类的外壳的搭建,结构是这样子的

package com.example.javapoetbutterknife;
public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
}

有包名,然后的类的一些配置,那我们知道等一下要实现的就是bind方法
先把上面收尾

).build().writeTo(filer);

其实就是写入Java文件,比如我这里的这个文件

image.png

不同版本的AS,位置不一样,不过基本都在 这个build文件夹,具体可以百度一下

回到我们上面的,现在要实现一个方法,先分析一下,这个方法首先需要一个参数,这个参数怎么获取,参数类型是什么,还有参数有了之后,我们要实现里面的findViewById方法,好,我们先看代码

 //生成一个名为target参数 类型就是上面的typeName
            ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(entry.getKey()), Constants.TARGET_PARAMETER_NAME)
                    .build();//target

            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.BIND_METHOD_NAME)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(parameterSpec);

            for (Element fieldElement : entry.getValue()) {
                String fieldName = fieldElement.getSimpleName().toString();
                //获取BindView上面的id值
                int viewId = fieldElement.getAnnotation(BindView.class).value();
                String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
                //拼接成target.tv=target.findViewById(R.id.tv)
                methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
                        Constants.TARGET_PARAMETER_NAME, viewId);
            }

又是一大坨,没事,我们先看第一句话

  ParameterSpec parameterSpec = ParameterSpec.builder(ClassName.get(entry.getKey()), Constants.TARGET_PARAMETER_NAME)
                    .build();//target

我们先看看这个Constants.TARGET_PARAMETER_NAME=public static final String TARGET_PARAMETER_NAME = "target";
其实就是声明一个参数类型 ClassName.get(entry.getKey())取出来的是MainActivity,然后这个对象ParameterSpec parameterSpec其实就是类似这个(MainActivity target),记得对比上面那个方法代码

继续走起

   MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constants.BIND_METHOD_NAME)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(parameterSpec);

这里构造了一个方法对象,首先老规矩看看这个 Constants.BIND_METHOD_NAME是啥

    public static final String BIND_METHOD_NAME = "bind";

其实就是方法名称bind,没错第一个参数传的就是方法 名称,然后是Override.class注解,方法访问权限为public,参数为(MainActivity target)上面生成的

最后最后啦 ,就是里面的方法 实现

 for (Element fieldElement : entry.getValue()) {
                String fieldName = fieldElement.getSimpleName().toString();
                //获取BindView上面的id值
                int viewId = fieldElement.getAnnotation(BindView.class).value();
                String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
                //拼接成target.tv=target.findViewById(R.id.tv)
                methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
                        Constants.TARGET_PARAMETER_NAME, viewId);
            }

首先 解释一下这个循环,因为一个类里面一般都不止一个控件需要绑定把,我们这里是demo所以我只写了一个TextView tv ,然后

String fieldName = fieldElement.getSimpleName().toString();

获取控件名称,比如这里fieldName ==tv
然后获取注解的ID

 //获取BindView上面的id值
                int viewId = fieldElement.getAnnotation(BindView.class).value();

我们demo就是R.id.tv的整型值

下面有个比较需要稍微了解JavaPoet的东西

  String methodContent = "$N." + fieldName + " = $N.findViewById($L)";

$N for Name就是名字 $L for Link就是引用 这里 $N就是target
$L 即使R.id.tv的整型值

最后

 //拼接成target.tv=target.findViewById(R.id.tv)
                methodBuilder.addStatement(methodContent, Constants.TARGET_PARAMETER_NAME,
                        Constants.TARGET_PARAMETER_NAME, viewId);

拼接成target.tv=target.findViewById(R.id.tv)

Ok 全剧终

大家想了解更多直接去我的github看代码实现

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容