美文网首页
Butterknife原理

Butterknife原理

作者: 旺仔_100 | 来源:发表于2021-07-04 12:08 被阅读0次

一、目标

写一个demo来实现Butterknife的findViewById功能。

二、核心原理以及实现

核心原理

通过@BindView注解来替代findViewById。主要是通过APT(注解解析器)在编译文件的时候查找到@BindView注解,获取到对应的控件引用和控件id,动态的生成java文件,里面会有个bindView方法,方法内容就是activity.findViewById(id)。java文件可以通过两种方式,一种是直接使用StringBuilder拼接,还有一种是通过javapoet的api来拼接。后者不会出现拼写错误。最后一步就是反射调用生成的bindview方法。

实现

项目主要分为四个模块

  • app 主要是用来测试我们功能实现的。
  • apt-annotation 是 一个java library,主要放置自定义注解@SensorsDataBindView
  • apt-processor 是我们注解处理器模块,它主要是根据apt-annotation模块中定义的注解,在编译时生成xxxActivity_sensorsDataViewBinding.java文件。该module需要依赖apt-annotation。
  • apt-sdk 是一个android module,它反射调用apt-process模块生成的xxxActivity_sensorsDataViewBinding.java中的方法,实现对View的绑定,该module需要依赖apt-annotation。

三、代码实现及对应讲解

a、创建一个java library apt-annotation

自定义注解@SensorsDataBindView

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

上述代码使用元注解@Retention和@Target
@Retention 定义该注解被保留的时间长短策略。

  • SOURCE 表示会被编译器忽略
  • CLASS 表示该注解将会被保留在Class文件中,但在运行时并不会被VM保留。这是一种默认行为,没有使用@Retention注解的注解都会采用这种策略。
  • RUNTIME 表示保留至运行时。所以我们可以通过反射去获取注解信息。
    @Tartget
    定义注解所修饰的对象范围。Annotation可用于packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量。
    Target主要有下面几种范围:
  • CONSTRUCTOR:用于描述构造器;
  • FIELD:用于描述域;
  • LOCAL_VARIABLE:用于描述局部变量;
  • METHOD:用于描述方法;
  • PACKAGE:用于描述包;
  • PARAMETER:用于描述参数;
  • TYPE:用于描述类、接口(包括注解类型)或者enum声明。
b、创建一个java library apt-processor

需要依赖auto-service注解

    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementation project(path: ':apt-annotation')

创建注解处理器,注意类上面需要添加注解@AutoService



/**
 * Element中定义的一些常用的方法
 * asType 返回此元素的种类:包、类、接口、字段、方法
 * getModifiers 返回此元素的修饰符号
 * getSimpleName 返回此元素的简单名称  如类名
 * getEnclosedElements 返回封装此元素的最里面元素
 * getAnnotation 返回此元素针对指定类型的注解
 * */

/***
 *Element 5个直接子类
 * TypeElement 一个类或者接口程序元素
 * VariableElement 代表一个字段、enum常量、方法、构造参数、局部变量或异常参数
 * ExecutableElement  某个类或者接口方法、构造方法或初始化程序(静态或实例),包括注解类型元素
 * PackageElement  一个包程序元素
 * ExecutableElement 某个类或者接口的方法、构造方法或者初始化程序(静态或者实例),包括注解类型元素
 * TypeParameterElement 一般类、接口、方法或构造方法元素的泛型参数
 *
 * */

@AutoService(Process.class)
public class SensorsDataBindViewProcessor  extends AbstractProcessor {
    private Elements elementUtils;
    private Map<String,SensorsDataClassCreateFactory> mClassCreatorFactoryMap = new HashMap<>();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //提供了很多工具类如 Elements \ Types \ Filer

        elementUtils = processingEnvironment.getElementUtils();
    }

    /**
     * 注解处理器是注册给哪个注解的
     * @return
     */

    @Override
    public Set<String> getSupportedAnnotationTypes() {

        HashSet<String> supportTypes = new LinkedHashSet<>();
        supportTypes.add(SensorsDataBindView.class.getCanonicalName());
        //CanonicalName : com.example.apt_annotation.SensorsDataBindView
        System.out.println("CanonicalName : " + SensorsDataBindView.class.getCanonicalName()) ;

        return supportTypes;
    }

    ///最小支持的java版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("process : " ) ;
        mClassCreatorFactoryMap.clear();
        //得到所有的注解

        Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(SensorsDataBindView.class);
        for (Element element : elementsAnnotatedWith) {
            //因为我们是方法注解,所以可以直接强转成VariableElement
            //VariableElement 代表一个字段、enum常量、方法、构造参数、局部变量或异常参数
            VariableElement variableElement = (VariableElement) element;
            //返回封装元素最里面的元素  就是类的全名
            TypeElement classElement = (TypeElement) variableElement.getEnclosingElement();
            //TypeElement : com.example.aptdemo.MainActivity
            System.out.println("TypeElement : "+classElement.toString());
            String fullClassName = classElement.getQualifiedName().toString();
            //fullClassName : com.example.aptdemo.MainActivity
            System.out.println("fullClassName : "+fullClassName);
            SensorsDataClassCreateFactory proxy = mClassCreatorFactoryMap.get(fullClassName);
            if(proxy == null){
                proxy = new SensorsDataClassCreateFactory(elementUtils,classElement);
                mClassCreatorFactoryMap.put(fullClassName,proxy);
            }

            SensorsDataBindView bindAnnotation = variableElement.getAnnotation(SensorsDataBindView.class);
            int id = bindAnnotation.value();
            proxy.putElement(id,variableElement);

        }

        //创建java文件
        for (String s : mClassCreatorFactoryMap.keySet()) {
            SensorsDataClassCreateFactory proxyInfo = mClassCreatorFactoryMap.get(s);
//            try {
//                //todo  xiecuolecreateSourceFile  写成了createClassFile
//                JavaFileObject jfo = processingEnv.getFiler().createSourceFile(proxyInfo.getProxyClassFullName(), proxyInfo.getTypeElement());
//
//                Writer writer = jfo.openWriter();
//                writer.write(proxyInfo.generateJavaCode());
//                writer.flush();
//                writer.close();
//                System.out.println("JavaFileObject : ");
//            } catch (IOException e) {
//                e.printStackTrace();
//                System.out.println("IOException : ");
//            }

            //javapoet
            JavaFile javaFile = JavaFile.builder(proxyInfo.getPackageName(), proxyInfo.generateJavaCodeWithJavaPoet()).build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

        return true;
    }
}
  • init 初始化函数,可以得到ProcessingEnvioment对象。ProcessingEnvironment提供很多有用的工具类,如Element、Types、Filer。
  • getSupportedAnnotedAnnotationTypes 指定这个注解处理器是注册给哪个注解的,这里指定的是我们上面创建的注解@SensorsDataBindView。
  • getSupportedSourceVersion 指定使用的java版本,通常都是返回SourceVersion.lastSupported。这个方法也可以使用@SupportedSourceVersion注解代替
  • process 这里可以扫描、评估、处理注解的代码,生产java文件。通过roundEnvironment.getElementsAnnotatedWith(SensorsDataBindView.class)可以得到所有含有@SensorsDataBindView注解的elements集合。可以将element强制转化为VariableElement类型,通过variableElement.getEnclosingElement()可以获取类的信息TypeElement,通过classElement.getQualifiedName().toString()可以获取完整的包名和类名。然后讲elements的信息以完整的包名和类名作为key保存到mClassCreateFactorFactoryMap中,最后通过mClassCreateFactoryMap创建对应的Java文件,其中mClassCreateFactoryMap是提供给SensorsDataClassCreateFactory的集合。

SensorsDataClassCreateFactory 主要是拼接生成java文件的,源码如下


public class SensorsDataClassCreateFactory {
    private String mBindingClassName;
    private String mPackageName;
    private TypeElement mTypeElement;
    private Map<Integer, VariableElement> mVariableElementMap = new HashMap<>();

    public SensorsDataClassCreateFactory(Elements elements,TypeElement mTypeElement) {
        this.mTypeElement = mTypeElement;
        PackageElement packageElement = elements.getPackageOf(mTypeElement);
        String packgeName = packageElement.getQualifiedName().toString();
        String className = mTypeElement.getSimpleName().toString();
        this.mPackageName = packgeName;
        this.mBindingClassName = className + "_SensorsDataViewBinding";

    }

    public void putElement(int id,VariableElement element){
        mVariableElementMap.put(id,element);
    }
    /**
     * 创建java代码
     */

    public String generateJavaCode() {
        StringBuilder builder = new StringBuilder();
        builder.append("/**\n" +
                " * Auto Created by SensorsData APT\n" +
                " */\n");
        builder.append("package ").append(mPackageName).append(";\n");
        builder.append('\n');
        builder.append("public class ").append(mBindingClassName);
        builder.append(" {\n");

        generateBindViewMethods(builder);
        builder.append('\n');
        builder.append("}\n");
        return builder.toString();
    }

    /**
     * 加入Method
     *
     * @param builder StringBuilder
     */
    private void generateBindViewMethods(StringBuilder builder) {
        builder.append("\tpublic void bindView(");
        builder.append(mTypeElement.getQualifiedName());
        builder.append(" owner ) {\n");
        for (int id : mVariableElementMap.keySet()) {
            VariableElement element = mVariableElementMap.get(id);
            String viewName = element.getSimpleName().toString();
            String viewType = element.asType().toString();
            builder.append("\t\towner.");
            builder.append(viewName);
            builder.append(" = ");
            builder.append("(");
            builder.append(viewType);
            builder.append(")(((android.app.Activity)owner).findViewById( ");
            builder.append(id);
            builder.append("));\n");
        }
        builder.append("  }\n");
    }

    public TypeSpec generateJavaCodeWithJavaPoet(){
        TypeSpec bindingClass = TypeSpec.classBuilder(mBindingClassName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(generateMethodsWithJavaPoet())
                .build();

        return  bindingClass;
    }

    public MethodSpec generateMethodsWithJavaPoet(){
        ClassName owner = ClassName.bestGuess(mTypeElement.getQualifiedName().toString());
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bindView")
        .addModifiers(Modifier.PUBLIC)
                .returns(void.class)
                .addParameter(owner,"owner");

        for (Integer integer : mVariableElementMap.keySet()) {
            VariableElement variableElement = mVariableElementMap.get(integer);
            String viewName = variableElement.getSimpleName().toString();
            String viewType = variableElement.asType().toString();
            methodBuilder.addCode("owner."+viewName+ " = "+"("+viewType+")(((android.app.Activity)owner).findViewById("+integer
                    +"));");
        }
        return methodBuilder.build();
    }

    public String getPackageName() {
        return mPackageName;
    }

    public String getProxyClassFullName(){
        return mPackageName + "." + mBindingClassName;
    }

    public TypeElement getTypeElement(){
        return mTypeElement;
    }
}
c.创建一个Android module apt-sdk

通过反射调用生成的SensorsDataViewBinding.java的bindView方法。
需要依赖apt-annottion

public class SensorsDataAPI {
    public static void bindView(Activity activity){
        Class<? extends Activity> aClass = activity.getClass();
        try {
            Class<?> bindViewClass = Class.forName(aClass.getName() + "_SensorsDataViewBinding");
            Method method = bindViewClass.getMethod("bindView", activity.getClass());
            method.invoke(bindViewClass.newInstance(),activity);

        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

到这里基本完成。源码上传到github:https://github.com/yangzai100/APTDemo/tree/master

相关文章

  • 开源框架_02ButterKnife

    参考文章 : ButterKnife使用和原理 深入理解ButterKnife源码并掌握原理(一) 深入理解But...

  • ButterKnife分析

    一、ButterKnife的使用 Android Butterknife使用方法总结 - 简书 二、原理浅析 Bu...

  • ButterKnife原理

    ButterKnife是用来解放开发者的,避免重复编写findViewById,setOnClickListene...

  • ButterKnife原理

    参考githubButterKnife是一个视图注入框架。解决问题:去除大量繁重的View对象查findViewB...

  • Butterknife原理

    一、目标 写一个demo来实现Butterknife的findViewById功能。 二、核心原理以及实现 核心原...

  • ButterKnife原理分析

    ButterKnife源码地址 ButterKnife的分析文章很多了,这里只是简单分析原理,不想看代码,可以直接...

  • ButterKnife原理与源码分析

    参考深入理解ButterKnife源码并掌握原理

  • Android面试题3

    1 OkHttp原理?2 Retrofit原理?为何用代理?代理的作用是什么?3 ButterKnife原理?用到...

  • 手写ButterKnife

    了解了ButterKnife的原理后,今天我就带领大家手写一个简易的ButterKnife。因为ButterKni...

  • ButterKnife(二): 原理解析篇

    在上一篇文章ButterKnife使用篇里面已经详细介绍过如何使用ButterKnife了,本篇文章将分析它的原理...

网友评论

      本文标题:Butterknife原理

      本文链接:https://www.haomeiwen.com/subject/josmultx.html