美文网首页
04.手写ButterKnife(ButterKnife源码阅读

04.手写ButterKnife(ButterKnife源码阅读

作者: 任振铭 | 来源:发表于2018-04-26 21:12 被阅读38次

    源码地址:https://github.com/renzhenming/MyButterknife

    相信大多数安卓人员开发中都在使用ButterKnife进行代码生成。使用ButterKnife有几个好处,第一,提高开发效率,可以一键生成布局中所有的View对象或者设置View的点击事件,这两个功能是我们最常用的。第二,不会影响app运行效率,ButterKnife采用编译时注解的方式生成代码,相对于XUtils的注解方式,在效率上有很大的优势,看过Xutils源码的话你会发现,里边的注解是反射的方式实现的,而反射在一定程度上是会增大运行的开销的。

    ButterKnife的作者是就职于谷歌的JakeWharton,在GitHub上可以看到完整的代码,https://github.com/JakeWharton/butterknife/tree/master/butterknife-annotations/src/main/java/butterknife,ButterKnife的功能是相当多的,不止是我们最长用的bindView set onClick等,看看这些注解就大概知道它所涉及到的功能点

    826815906.png

    今天打算实现的是其中一个BindView注解,后期可能会实现onClick方法,借此来加深一下编译时注解的使用

    当我们在一个Activity中使用ButterKnife.bind()之后,重新编译会生成这样一个类,xxxx_ViewBinding,xxxx代表的是当前Activity的名字,这个类是什么样的有什么作用,我们简单拷贝一个来看

    public final class MainActivity_ViewBinding implements Unbinder {
      private MainActivity target;
    
      MainActivity_ViewBinding(MainActivity target) {
        this.target = target;
        target.world = Utils.findViewById(target,2131427424);
        target.bitch = Utils.findViewById(target,2131427425);
      }
    
      @Override
      @CallSuper
      public final void unbind() {
        target.world = null;
        target.bitch = null;
      }
    }
    

    从这个类上可以看到,使用ButterKnife并非不用findViewById了,而是把这个工作交给了编译器去自动生成,然后在bind方法调用的时候,反射一次创建这个自动生成的类的对象从而实现view注入的功能,那么为什么编译时会自动生成类呢,这要用到注解的知识,接下来我们开始自己手写一个简单的ButterKnife工具

    我们仿照JakeWharton的分包形式,创建一个butterknife-annotation和butterknife-compiler的Java library(butterknife-compiler必须时Java library,因为注解生成器需要继承AbstractProcessor,这个类只有Java工程可以引用到),我们在butterknife-annotation中创建注解BindView

    /**
     * Created by renzhenming on 2018/4/24.
     */
    @Target(ElementType.FIELD)
    @Retention(RetentionPolicy.CLASS)
    public @interface BindView {
        int value();
    }
    

    在butterknife-compiler中创建注解生成器

    
    /**
     * Created by renzhenming on 2018/4/24.
     * AbstractProcessor这个类是Java中的,只能在ava Library中使用
     */
    @AutoService(Processor.class)
    public class ButterKnifeProcessor extends AbstractProcessor {
    
        private Filer mFiler;
        private Elements mElementUtils;
    
        /**
         * init()方法会被注解处理工具调用,并输入ProcessingEnviroment参数。
         * ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
         * @param processingEnv 提供给 processor 用来访问工具框架的环境
         */
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            mFiler = processingEnv.getFiler();
            mElementUtils = processingEnv.getElementUtils();
        }
    
        /**
         * 指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6
         * @return  使用的Java版本
         */
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
        /**
         * 这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称
         * @return  注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
         */
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            //仿照Butternife源码
            Set<String> types = new LinkedHashSet<>();
            for (Class<? extends Annotation> annotation: getSupportAnnotations()){
                 types.add(annotation.getCanonicalName());
            }
            return types;
        }
    
        private Set<Class<? extends Annotation>> getSupportAnnotations() {
            Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
            annotations.add(BindView.class);
            return annotations;
        }
        /**
         * 这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
         * 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素
         * @param set   请求处理的注解类型
         * @param roundEnvironment  有关当前和以前的信息环境
         * @return  如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;
         *          如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
         *
         *          这里的log信息只能在gradle console中看到,Android logcat看不到。要注意
         */
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
              ...........................
        }
    }
    
    

    注意注解生成器的这个library build.gradle的配置

    dependencies {
        compile fileTree(include: ['*.jar'], dir: 'libs')
        //AutoService注解使用
        compile 'com.google.auto.service:auto-service:1.0-rc3'
        //自动生成类相关
        compile 'com.squareup:javapoet:1.7.0'
        compile project(':butterknife-annotation')
    }
    //解决 错误: 编码GBK的不可映射字符 的报错
    tasks.withType(JavaCompile){
        options.encoding='UTF-8'
    }
    //android studio不完全能兼容Java8,所以指定编译版本位1.7
    sourceCompatibility = "1.7"
    targetCompatibility = "1.7"
    

    在我们项目主工程的app 配置文件build.gradle中设置依赖

    compile project(':butterknife-annotation')
    compile project(':butterknife-compiler')
    

    这时候编译一下工程就可以执行到ButterKnifeProcessor 的process方法中了,如果看不到打印,检查两点
    第一:目标文件(bind绑定的activity)是否相比于上一次编译有修改,如果代码没有改变,不会重新编译
    第二:因为是Java library中打印的log,在Android studio的logcat中是无法看到的,你需要到Gradle Console中去看

    process方法有执行了,那么进行下一步,编写自动生成类的代码,在process方法中加入如下代码

    //------------获取注解-----------
    
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
            //LinkedHashMap输出和输入的顺序相同,先输入就先输出
            Map<Element,List<Element>> elementsMap = new LinkedHashMap<>();
            for (Element element : elements) {
                //这里会把所有跟注解有关的field全部拿到,包括各个类中的field,也就是说在
                //编译时,项目中所有涉及到这个注解的地方的所有field都在这个Set中返回了,
                //我们需要手动进行分类
                System.out.println("-----------------------"+element.getSimpleName());
                //得到的enclosingElement是这个field所在类的类名
                Element enclosingElement = element.getEnclosingElement();
                System.out.println("------------enclosingElement-----------"+enclosingElement.getSimpleName());
                //以类名位key值存储一个类中所有的field到集合中
                List<Element> bindViewElements = elementsMap.get(enclosingElement);
                if (bindViewElements == null){
                    bindViewElements = new ArrayList<>();
                    elementsMap.put(enclosingElement,bindViewElements);
                }
                bindViewElements.add(element);
            }
    
            //------------生成代码-----------
    
            for (Map.Entry<Element,List<Element>> entry:elementsMap.entrySet()){
                Element enclosingElement = entry.getKey();
                List<Element> bindViewElements = entry.getValue();
    
                ClassName unbinderClassName = ClassName.get("com.rzm.butterknife","Unbinder");
                System.out.println("------------Unbinder-----------"+unbinderClassName.simpleName());
                //得到类名的字符串
                String activityName = enclosingElement.getSimpleName().toString();
                ClassName activityClassName = ClassName.bestGuess(activityName);
                //拼装这一行代码:public final class xxx_ViewBinding implements Unbinder
                TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityName+"_ViewBinding")
                        //类名前添加public final
                        .addModifiers(Modifier.FINAL,Modifier.PUBLIC)
                        //添加类的实现接口
                        .addSuperinterface(unbinderClassName)
                        //添加一个成员变量,这个名字target是仿照butterknife
                        .addField(activityClassName,"target",Modifier.PRIVATE);
    
                //实现Unbinder的方法
                //CallSuper这个注解不像Override可以直接拿到,需要用这种方式
                ClassName callSuperClass = ClassName.get("android.support.annotation","CallSuper");
                MethodSpec.Builder unbindMethod = MethodSpec.methodBuilder("unbind")//和你创建的Unbinder中的方法名保持一致
                        .addAnnotation(Override.class)
                        .addAnnotation(callSuperClass)
                        .addModifiers(Modifier.FINAL, Modifier.PUBLIC);
    
                //添加构造函数
                MethodSpec.Builder constructMethodBuilder = MethodSpec.constructorBuilder()
                        .addParameter(activityClassName,"target");
                constructMethodBuilder.addStatement("this.target = target");
                for (Element bindViewElement : bindViewElements) {
                    String fieldName = bindViewElement.getSimpleName().toString();
    
                    //在构造方法中添加初始化代码
                    ClassName utilsClassName = ClassName.get("com.rzm.butterknife", "Utils");
                    BindView annotation = bindViewElement.getAnnotation(BindView.class);
                    int resId = annotation.value();
                    constructMethodBuilder.addStatement("target.$L = $T.findViewById(target,$L)",fieldName,utilsClassName,resId);
    
                    //在unbind方法中添加代码 target.textView1 = null;
                    //不能用addCode,因为它不会在每一行代码后加分号和换行
                    unbindMethod.addStatement("target.$L = null",fieldName);
                }
                classBuilder.addMethod(constructMethodBuilder.build());
    
    
                classBuilder.addMethod(unbindMethod.build());
    
                //开始生成
                try {
    
                    //得到包名
                    String packageName = mElementUtils.getPackageOf(enclosingElement)
                            .getQualifiedName().toString();
    
                    JavaFile.builder(packageName,classBuilder.build())
                            //添加类的注释
                            .addFileComment("butterknife 自动生成")
                            .build().writeTo(mFiler);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            return false;
    

    创建一个butterknife安卓library,创建类ButterKnife,实现bind方法,通过反射将自动生成的类生成注入view

    public class ButterKnife {
    
        public static Unbinder bind(Activity activity){
            try {
                Class<? extends Unbinder> clazz = (Class<? extends Unbinder>) Class.forName(activity.getClass().getName() + "_ViewBinding");
                //构造函数
    
                Constructor<? extends Unbinder> unbinderConstuctor = clazz.getDeclaredConstructor(activity.getClass());
                Unbinder unbinder = unbinderConstuctor.newInstance(activity);
                return unbinder;
            } catch (Exception e) {
                e.printStackTrace();
            }
            return Unbinder.EMPTY;
        }
    }
    

    Unbinder
    ···
    public interface Unbinder {

    //仿照butterknife源码
    
    @UiThread
    void unbind();
    
    Unbinder EMPTY = new Unbinder() {
        @Override
        public void unbind() {
    
        }
    };
    

    }
    ···
    Utils

    public class Utils {
        public static <T extends View> T findViewById(Activity activity,int viewId){
            return (T)activity.findViewById(viewId);
        }
    }
    

    测试一下使用

    public class MainActivity extends AppCompatActivity {
    
        @BindView(R.id.text1)
        TextView world;
    
        @BindView(R.id.text2)
        TextView bitch;
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ButterKnife.bind(this);
            world.setText("aaaaa");
        }
    }
    

    重新编译一下,可以看到自动生成的类


    828223296.png

    运行成功,至此我们不需要写findViewById也可以得到view对象了

    相关文章

      网友评论

          本文标题:04.手写ButterKnife(ButterKnife源码阅读

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