Android进阶之路-自定义ButterKnife(中)

自定义ButterKnife(中)使用Javapoet库动态生成java文件

自定义butterknife(上)
自定义butterknife(中)
自定义butterknife(下)

上篇之后,我们的自定义注解处理器已经可以读取到我们的注解了,但是没有内容。中篇就是带着大家动态生成java类XXX$ViewBinder,实现ButterKnife功能。(这里暂时还没有封装层API,所以看起来不是很像,下篇进行封装,就很像了。)
在这里插入图片描述
在这里插入图片描述

背景

如果了解ButterKnife的原理可以跳过下面的斜体字

众所周知,android项目通过gradle构建工具编译打包的时候,会执行很多task任务。大致会经历以下几个阶段
.java文件–>.class文件–>.dex文件–>.apk文件
ButterKnife的原理就是编译的时候动态生成XXX$ViewBinder类,在我们调用BufferKnife.bind(this)的时候,通过动态的类加载方式创建出这个ViewBinder类的实例,并且调用bind()方法完成给带有注解的对象进行id绑定的操作。

动态生成java文件我们也可以不用这个库,但是容易出错。
Javapoet这个库可以很方便地帮我们的构建一个java文件,下面我们就用这个库来帮我们生成一个XXX$ViewBinder

正文

完善自定义注解
获取到所有的注解,并且按所属文件名区分
完善注解编译器,生成文件

完善自定义注解,让其接收view的 id
GavinBindView

// SOURCE 注解仅在源码中保留,class文件中不存在
// CLASS 注解在源码和class文件中都存在,但运行时不存在
// RUNTIME 注解在源码,class文件中存在且运行时可以通过反射机制获取到
@Target(ElementType.FIELD) // 注解作用在属性之上
@Retention(RetentionPolicy.CLASS) // 编译期原理(交予注解处理器)
public @interface GavinBindView {
    // 返回R.id.xx值
    int value();
}

编写生成文件的逻辑

那在初始化方法中获取几个比较重要的成员变量
GavinButterknifeProcessor

//..
public class GavinButterknifeProcessor extends AbstractProcessor {
    // 操作Element工具类 (类、函数、属性都是Element)
    private Elements elementUtils;

    // type(类信息)工具类,包含用于操作TypeMirror的工具方法
    private Types typeUtils;

    // Messager用来报告错误,警告和其他提示信息
    private Messager messager;

    // 文件生成器 类/资源,Filter用来创建新的类文件,class文件以及辅助文件
    private Filer filer;

    // key:类节点, value:被@BindView注解的属性集合
    private Map<TypeElement, List<VariableElement>> tempBindViewMap = new HashMap<>();
    // 该方法主要用于一些初始化的操作,通过该方法的参数ProcessingEnvironment可以获取一些列有用的工具类
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        // 初始化
        elementUtils = processingEnvironment.getElementUtils();
        typeUtils = processingEnvironment.getTypeUtils();
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();
        messager.printMessage(Diagnostic.Kind.NOTE,
                "注解处理器初始化完成,开始处理注解------------------------------->");
    }
//...
}

process方法中我们做两件事
1、获取代码中使用了GavinBindView注解的元素集合,按照类名分组elementsInGroup(bindViewElements);
2、动态生成XXX$ViewBinder文件createJavaFile();

关于生成代码的api我们不常用,所以理解起来困难也很正常,多看几次就好了。耐心读代码和注释,会有很大的收获。已经将完整的源码上传到github,配合使用,效果更佳。

//..
public class GavinButterknifeProcessor extends AbstractProcessor {
    //..
    /*代码中几个常量
    public class Constant {
    public static final String CODE_CLASS_EXT_NAME = "$ViewBinder";
    public static final String CODE_PARAM = "target";
    public static final String CODE_METHOD_NAME = "bind";
}*/
       @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("GavinButterknifeProcessor.process...");

        // 一旦有属性上使用@BindView注解
        if (!SimpleUtils.isEmpty(set)) {
            // 获取所有被 @BindView 注解的 元素集合
            Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(GavinBindView.class);
            elementsInGroup(bindViewElements);
            try {
                createJavaFile();
            } catch (IOException e) {
                e.printStackTrace();
                messager.printMessage(Diagnostic.Kind.NOTE, "@IOException >>> " + e.getMessage());
            }
        }

        return false;
    }

    /**
     * 分类,key为类名,value为注解的Element集合
     * {"Sample1Activity":[textview1Element,textview2Element]
     * ,"Sample2Activity":[textview1Element,textview2Element]}
     *
     * @param bindViewElements
     */
    private void elementsInGroup(Set<? extends Element> bindViewElements) {
        if (!SimpleUtils.isEmpty(bindViewElements)) {
            for (Element element : bindViewElements) {
                messager.printMessage(Diagnostic.Kind.NOTE, "@BindView >>> " + element.getSimpleName());
                if (element.getKind() == ElementKind.FIELD) {
                    VariableElement fieldElement = (VariableElement) element;
                    // 注解在属性之上,属性节点父节点是类节点
                    TypeElement enclosingElement = (TypeElement) fieldElement.getEnclosingElement();
                    // 如果map集合中的key:类节点存在,直接添加属性
                    if (tempBindViewMap.containsKey(enclosingElement)) {
                        tempBindViewMap.get(enclosingElement).add(fieldElement);
                    } else {
                        List<VariableElement> fields = new ArrayList<>();
                        fields.add(fieldElement);
                        tempBindViewMap.put(enclosingElement, fields);
                    }
                }
            }
        }
    }

    void createJavaFile() throws IOException {
        TypeElement viewBinderType = elementUtils.getTypeElement(GavinViewBinder.class.getName());

        for (Map.Entry<TypeElement, List<VariableElement>> entry :
                tempBindViewMap.entrySet()) {
            //类名 TestActivity
            ClassName className = ClassName.get(entry.getKey());

            //实现接口泛型 implements GavinViewBinder<TestActivity>
            ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBinderType), className);

            //参数体配置(final TestActivity target)
            ParameterSpec parameterSpec = ParameterSpec.builder(className, Constant.CODE_PARAM)
                    .addModifiers(Modifier.FINAL)
                    .build();

            //声明方法
            MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constant.CODE_METHOD_NAME)
                    .addAnnotation(Override.class)
                    .addModifiers(Modifier.PUBLIC)
                    .addParameter(parameterSpec);

            for (Element fieldElement :
                    entry.getValue()) {
                //获取属性名
                String fieldName = fieldElement.getSimpleName().toString();
                //获取@GavinBindView注解的值
                int annotationValue = fieldElement.getAnnotation(GavinBindView.class).value();
                messager.printMessage(Diagnostic.Kind.NOTE, fieldName + ".annotationValue >>> " + annotationValue);
                //target.tv = target.findViewById(R.id.tv);
                String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
                methodBuilder.addStatement(methodContent,
                        Constant.CODE_PARAM,
                        Constant.CODE_PARAM,
                        annotationValue);

                messager.printMessage(Diagnostic.Kind.NOTE, methodBuilder.toString());
            }

            //生成类
            TypeSpec typeSpec = TypeSpec.classBuilder(className.simpleName() + Constant.CODE_CLASS_EXT_NAME)//class XXX$ViewBinder
                    .addSuperinterface(typeName)//implements GavinViewBinder<XXXActivity>
                    .addModifiers(Modifier.PUBLIC)//public
                    .addMethod(methodBuilder.build())// 加入前面的方法
                    .build();

            //生成文件
            JavaFile javaFile = JavaFile.builder(className.packageName(), typeSpec)
                    .build();
            javaFile.writeTo(filer);
        }
    }
//...
}

代码写好,我们引入主工程测试一下
项目结构

public class TestActivity extends AppCompatActivity {

    @GavinBindView(R.id.textView)
    public TextView textView;
    @GavinBindView(R.id.textView2)
    public TextView textViewxx;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        new TestActivity$ViewBinder().bind(this);
        textView.setText("没有手动findById");
        textViewxx.setText("未封装api,直接使用Javapoet生成的类");
    }
}

点击构建
在这里插入图片描述
完成后即可看见生成的java文件,生成的文件在 app/build/generated/ap_generated_sources/debug/out/包名/xxx
如果没有找到,那就按照类名去搜索
在这里插入图片描述

福利

些同学比较急着看效果,我标记出来给大家看看。
在这里插入图片描述

拓展

后面会带大家进入封装上传到公共仓库提供给其他小伙伴使用。请看
Android进阶之路-自定义ButterKnife(下)

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值