一个例子理解 Butterknife 基本原理

作者: __Y_Q | 来源:发表于2020-09-25 18:14 被阅读0次

    说起 ButterKnife 相信大多人都知道这么一个框架, 它是一个专注于 Android 系统的 View 注入框架, 简化了我们的 findViewById, OnClick, getString() 以及加载动画等操作, 给平时开发带来了很大的便利. 只是现在这个框架的作者已经不再更新了, 只会修复一些关键性的 BUG, 同时建议使用 Googleview binding 了. 但是作为曾经最流行的框架之一, 还是很有必要学习和研究一下的.

    众所周知 ButterKnife 的便利来自于注解. 那么既然存在注解, 注解处理器技术的使用是必然的. 现在的框架不同于以往.
    以前的框架类似 XUtils 的注解很大程度上是使用反射来解析的, 反射带来性能消耗还是有的.
    但是现在, 大多数的注解框架都是基于 AnnotationProcessor 的编译时解析实现的.
    试想一下, 在程序编译时就完成了注解解析的工作, 又会给性能带来什么影响呢?答案当然是没影响。
    (APT 已不再被作者所维护, 并且 Google 推出了AnnotationProcessor 来替代它,更是集成到了 API 中)

    • 那么什么是 AnnotationProcessor呢 ?
      是一个 JavaC 的工具,也就是 Java 编译源码到字节码的一个预编译工具, 会在代码编译的时候调用到. 它有一个抽象类 AbstractProcessor, 只需实现该抽象类, 就可以在预编译的时候被编译器调用, 就可以在预编译的时候完成一下你想完成工作. 比如代码注入!!

    那么简单来说 ButterKnife 在编译时解析注解, 通过使用 AnnotationProcessor 代码注入. 这是最基本最核心的思想. 那么今天我们也来按照这个核心思想来写一个山寨版的 ButterKnife.
    先来看一下, 我们最终需要自动生成的文件内容是什么样的.

    public final class MainActivity_ViewBinding implements Unbinder {
      private MainActivity target;
    
      MainActivity_ViewBinding(MainActivity target) {
        this.target = target;
        target.tv_name = Utils.findViewById(target, 2131165425);
      }
    
      @Override
      @CallSuper
      public final void unbind() {
        MainActivity target = this.target;
        if (target == null) throw new IllegalStateException("Bindings already cleared. ");;
        target.tv_name = null;
      }
    }
    

    先来分析一波:
    首先自动生成的类实现了 Unbinder接口, 并且实现了 unbind 方法.
    有一个有参的构造函数,参数为 Activity, 在构造函数内对传入的 Activity 内的控件 ID , 进行 findById 操作. 有一个 Activity 类型的变量 target.
    OK, 现在就根据上面的这些代码, 开始愉快的山寨吧.

    先创建如下 Module

    1. 创建 App 名字为 butterknife-app,
    2. 创建 android Module 名字为 butterknife. 为 APP 提供 butterknife 绑定操作.
    3. 创建 java Module 名字为 butterknife-annotation. 存放我们声明的注解
    4. 创建 java Module 名字为 butterknife-compiler. 作为我们的注解处理器.

    工程目录截图如下


    目录

    APP 添加依赖

    implementation project(path: ':butterknife-annotations')
    implementation project(path: ':butterknife'
    annotationProcessor project(path: ':butterknife-compiler')
    

    butterknife-compiler 添加依赖

    implementation 'com.squareup:javapoet:1.13.0'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
    implementation 'com.google.auto.service:auto-service:1.0-rc7'
    implementation project(path: ':butterknife-annotations')
    
    • javapoetsquare 推出的开源 java 代码生成框架, 提供 Java Api 生成 .java 源文件. 这个框架功能非常有用, 我们可以很方便的使用它根据注解, 数据库模式, 协议格式等来对应生成代码. 通过这种自动化生成代码的方式, 可以让我们用更加简洁优雅的方式要替代繁琐冗杂的重复工作
    • auto-serviceGoogle 为我们提供用于 java.util.ServiceLoader 样式的服务提供者的配置/元数据生成器. 简单来说就是会为了加了 @AutoService 注解的类, 自动装载和实例化,并完成模块的注入.

    OK, 现在开始撸代码.

    1. 编写注解

    先到 butterknife-annotation module 中新建一个注解. em...既然山寨了, 那就连注解名字也一起山寨吧.

    package com.butterknife_annotations;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.CLASS) 
    public @interface BindView {
        int value();
    }
    

    原版中定义了很多注解, 什么 BindView, BindFont, BindInt, BindString, BindColor ... 扩展了很多很多, 这里我们就先写一个简单又山寨的 BindView 好了.

    2. 编写接口与工具类

    butterknife module 中新建

    1. Unbinder 接口, 声明一个 unbind 解绑方法. 待会要让我们自动生成的 java 文件实现这个接口.
    2. Utils 工具类, 实现我们真正的 findViewById.在注解处理器中调用.
    3. ButterKnife 先空着. 最后再写.
    public interface Unbinder {
        @UiThread
        void unbind();
    
        Unbinder EMPTY = new Unbinder() {
            @Override
            public void unbind() {
    
            }
        };
    }
    
    //在注解处理器重调用
    public class Utils {
        public static <T extends View> T findViewById(Activity activity, int id) {
            return activity.findViewById(id);
        }
    }
    

    3. 编写注解处理器

    接下来开始到 butterknife-compiler moudle 中写我们的注解处理器.
    新建一个 javaButterKnifeProcessor, 继承自 AbstractProcessor, 并重写如下方法

    @AutoService(Processor.class)
    public class ButterKnifeProcessor extends AbstractProcessor {
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            return false;
        }
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return super.getSupportedSourceVersion();
        }
    
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            return super.getSupportedAnnotationTypes();
        }
    }
    
    • init 方法主要做一些初始化的事情, 其中参数 processingEnv 会为我们提供很多有用的工具类
      • 例如等下需要用到的 Filer, 它用来生成 java 类文件.
      • Elements 注解处理器运行扫描源文件时, 以获取元素 (Element)相关的信息. Element 有以下几个子类:
        包 (PackageElement), 类 (TypeElement), 成员变量 (VariableElement), 方法 (ExecutableElement)
    • getSupportedSourceVersion方法 返回当前系统支持的 java 版本
    • getSupportedAnnotationTypes 该方法返回一个 Set<String>, 代表 ButterKnifeProcessor 要处理的注解类的名称集合,即 ButterKnife 支持的注解
    • process 敲黑板, 划重点, 这个就是最重要的方法. 在这里完成了目标类信息的收集并生成对应 java

    接着开始写下面几个简单的.

    //创建文件的时候需要用到
    private Filer mFiler;
    private Elements mElementUtils;
    //打印输出
    private Messager mMessager;
    
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
    }
    
     //指定处理的版本
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
    
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annnotation : getSupportedAnnotation()) {
            types.add(annnotation.getCanonicalName());
        }
        return types;
    }
    
    //添加所有我们需要处理的注解.
    public Set<Class<? extends Annotation>> getSupportedAnnotation() {
        Set<Class<? extends Annotation>> annotation = new LinkedHashSet<>();
        annotation.add(BindView.class);
        //原版本中会添加 N 多注解类到这里
        //例如 annotation.add(BindString.class)
        return annotation;
    }
    

    接下来就是最关键的 process 方法了.
    OK, 现在开始生成.

    /**
     * 获取每个 Activity 内所有加了需要解析的注解的元素
     * @param elements 我们所需要的注解集合, 因为目前我们就一个注解, 所以这里长度为 1.
     * @return key 是包含我们需要解析的注解所属的 Activity, value 为当前 Activit 内所有加了要解析的注解的元素.
     */
    private  Map<Element, List<Element>> getAllElements(Set<? extends Element> elements){
        Map<Element, List<Element>> elementMap = new LinkedHashMap<>();
        for (Element element : elements) {
            //来自那个 Activity=
            Element enclosingElement = element.getEnclosingElement();
            //以 Activity 为 Key, 先取一次,看 Map 中是否已存在
            List<Element> viewBindElement = elementMap.get(enclosingElement);
            if (viewBindElement == null) {
                //没有存在就重新创建
                viewBindElement = new ArrayList<>();
                //存入到 Map 中. key 为 Activity 名字, value 为 集合
                elementMap.put(enclosingElement, viewBindElement);
            }
            //存到集合, 同时也会更新 Map 中对应的集合
            viewBindElement.add(element);
        }
        return elementMap;
    }
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //获取所有 Activity 中加了 bindView 注解的 元素, 需要整理为一个 Activity 对应一个自己内部的元素集合
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
        Map<Element, List<Element>> elementMap = getAllElements(elements);
        return false;
    }
    

    通过 roundEnv.getElementsAnnotatedWith(BindView.class) 可以拿到所有加了 @BindView 注解的控件名字, 来自哪个 Activity. 但是一个 Activity 中可能有很多很多加了注解的控件, 那么我们需要整理成为一个 Map, 对每个 Activity 进行归类. 因为后面,我们需要为每个 Activity 都生成一个文件.

    那么接下来就需要开始遍历这个 Map, 开始为每个 Activity 都生成一个 java 文件. 有多少个 key 就生成多少个.

    for (Map.Entry<Element, List<Element>> entry : elementMap.entrySet()) {
        Element enclosingElement = entry.getKey();
        List<Element> viewBindElements = entry.getValue();
        //获得类名
        String activityClassNameStr = enclosingElement.getSimpleName().toString();
        mMessager.printMessage(Diagnostic.Kind.NOTE, "------->" + activityClassNameStr);
        //获取我们要实现的接口.
        ClassName unbinderClassName = ClassName.get("com.butterknife", "Unbinder");
        //获得对应类名的对象
        ClassName activityClassName = ClassName.bestGuess(activityClassNameStr);
        
        //开始生成类名, 继承接口对象, 以及字段
        //public final class MainActivity_ViewBinding implements Unbinder, 生成出来的样子是这样的
        TypeSpec.Builder classBuilder = buildClass(activityClassNameStr, unbinderClassName, activityClassName);
        
        //生成要实现的 unbinder 方法
        MethodSpec.Builder unbinderMethodBuilder = buildMethod(activityClassName);    
       
        //生成构造函数
        MethodSpec.Builder constructorMethodBuilder = buildConstructor(activityClassName,viewBindElements,unbinderMethodBuilder);
    
        //将方法添加到类中
        classBuilder.addMethod(unbinderMethodBuilder.build());
        //将构造函数添加到类中
        classBuilder.addMethod(constructorMethodBuilder.build());
    
      //开始生成类文件
      try {
          String packageName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
          mMessager.printMessage(Diagnostic.Kind.NOTE, "------->" + packageName);
          JavaFile.builder(packageName, classBuilder.build())
                  .addFileComment("自动生成")
                  .build().writeTo(mFiler);
      } catch (IOException e) {
          e.printStackTrace();
      }    
    }
    

    过程比较简单, 就是依次生成类, 方法, 如果需要构造函数的话, 也需要生成. 最后都添加到类的构造器中. 最后生成.
    下面是三个生成的方法 buildClass , buildMethod, buildConstructor.

    private TypeSpec.Builder buildClass(String activityClassNameStr, ClassName unbinderClassName, ClassName activityClassName) {
       return TypeSpec.classBuilder(activityClassNameStr + "_ViewBinding")
                //生成类的访问修饰符为 public final
                .addModifiers(Modifier.FINAL, Modifier.PUBLIC)
                //生成的类实现接口
                .addSuperinterface(unbinderClassName)
                //添加字段
                .addField(activityClassName, "target", Modifier.PRIVATE);
    }
    
    private MethodSpec.Builder buildMethod(ClassName activityClassName) {
        //生成类实现类 unbinder 方法
        ClassName callSuper = ClassName.get("androidx.annotation", "CallSuper");
        return MethodSpec.methodBuilder("unbind")
                .addModifiers(Modifier.FINAL, Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addAnnotation(callSuper)
                .addStatement("$T target = this.target", activityClassName)
                .addStatement("if (target == null) throw new IllegalStateException(\"Bindings already cleared. \");");
    }
    
    private MethodSpec.Builder buildConstructor(ClassName activityClassName, List<Element> viewBindElements, MethodSpec.Builder unbinderMethodBuilder) {
        MethodSpec.Builder constructorMethodBuilder = MethodSpec.constructorBuilder()
                .addParameter(activityClassName, "target")
                .addStatement("this.target = target");
    
        for (Element viewBindElement : viewBindElements) {
            //获得在 Activity 中声明的控件名字
            String filedName = viewBindElement.getSimpleName().toString();
            //拿到工具类的对象
            ClassName utilsClassName = ClassName.get("com.butterknife", "Utils");
            //拿到在 Activity 中注解中传入的参数 ID .
            int resId = viewBindElement.getAnnotation(BindView.class).value();
            constructorMethodBuilder.addStatement("target.$N = $T.findViewById(target, $L)", filedName, utilsClassName, resId);
            //最终生成的结果如下
            //target.tv_name = Utils.findViewByid(mainactivity, R.id.tv_name);
            //在 unbind 方法中,将控件全赋值为 Null
            unbinderMethodBuilder.addStatement("target.$N = null", filedName);
        }
        return constructorMethodBuilder;
    }
    

    最后一步, Activity 在使用的时候需要进行绑定. 需要传入当前 Activity 对象. 绑定的目的是什么呢? 就是根据传入的当前 Activity 然后调用生成文件的构造方法.
    OK, 我们继续到 butterknife module 中的 ButterKnife.java 添加方法 bind(Activity activity)

    public class ButterKnife {
        public static Unbinder bind(Activity activity) {
            try {
                //唯一需要的反射, 反射自动生成类的构造函数
                Class<? extends Unbinder> bindClassName = (Class<? extends Unbinder>) Class.forName(activity.getClass().getName() + "_ViewBinding");
                //调用自动生成类的构造函数
                Constructor<? extends Unbinder> bindConstructor = bindClassName.getDeclaredConstructor(activity.getClass());
                Unbinder unbinder = bindConstructor.newInstance(activity);
                return unbinder;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return Unbinder.EMPTY;
        }
    }
    

    最后在项目中 Build --> Clean Project --> Make Project, 在我们 APP 工程的目录下就能看到自动生成的代码文件了.

    目录

    迫不及待的来使用一把

    public class MainActivity extends AppCompatActivity {
        @BindView(R.id.tv_name)
        TextView mTvName;
        private Unbinder mUnbinder;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mUnbinder = ButterKnife.bind(this);
            mTvName.setText("666");
        }
        @Override
        protected void onDestroy() {
            super.onDestroy();
            mUnbinder.unbind();
        }
    }
    

    我这边运行成功了, 你们呢?

    OK, 到这里, 是不是对整个流程有个大致的印象了呢.

    1. 我们在我们声明的控件上添加注释.
    2. MainActivity 中调用 ButterKnife.bind(this) 传入当前 Activity.
    3. 编译的时候自动生成 MainActivity_ViewBinding 文件.
    4. 运行的时候,执行 ButterKnife.bind(this), 在 ButterKnife.bind() 方法中, 反射获取到自动生成的 MainActivity_ViewBinding 文件实例, 调用自动生成文件的构造方法. 在构造方法内执行 findViewById. 这样就获取到啦.

     
    好了, 就先山寨到这里吧. 其中还有一些没弄的, 比如加注解的控件不能声明为 private, 比如我们拿到的 ID 不是 R.id.xxx, 而是一堆数字的样子. 还有很多很多. 但是也算是基本完成了最核心最基础的功能. 并且也算是弄清楚了基本流程.

    相关文章

      网友评论

        本文标题:一个例子理解 Butterknife 基本原理

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