【腾讯Bugly干货分享】深刻理解 ButterKnife,让你的程序学会写代码

本文来自于腾讯bugly开发者社区,非经做者赞成,请勿转载,原文地址:http://dev.qq.com/topic/578753c0c9da73584b025875java

0、引子

话说咱们作程序员的,都应该多少是个懒人,咱们老是想办法驱使咱们的电脑帮咱们干活,因此咱们学会了各式各样的语言来告诉电脑该作什么——尽管,他们有时候也会误会咱们的意思。android

忽然有一天,我以为有些代码其实,能够按照某种规则生成,但你又不能不写——不是全部的重复代码均可以经过重构并采用高端技术好比泛型来消除的——好比我最痛恨的代码:git

TextView textView = (TextView) findViewById(R.id.text_view);
Button button = (Button) findViewById(R.id.button);

这样的代码,你总不能不写吧,真是让人沮丧。忽然想到之前背单词的故事:正着背背不过 C,倒着背背不过 V。嗯,也许写 Android app,也是写不过 findViewById 的吧。。程序员

咱们今天要介绍的 ButterKnife 其实就是一个依托 Java 的注解机制来实现辅助代码生成的框架,读完本文,你将可以了解到 Java 的注解处理器的强大之处,你也会对 dagger2 和 androidannotations 这样相似的框架有必定的认识。github

一、初识 ButterKnife

1.1 ButterKnife 简介

说真的,我一直对于 findViewById 这个的东西有意见,后来见到了 Afinal 这个框架,因而咱们就能够直接经过注解的方式来注入,哇塞,终于能够跟 findViewById 说『Byte Byte』了,真是好开心。面试

什么?寨见不是介么写么?缓存

不过,毕竟是移动端,对于用反射实现注入的 Afinal 之类的框架,咱们老是不免有一种发自心里的抵触,因而。。。微信

title

别哭哈,不用反射也能够的~~网络

这个世界有家神奇的公司叫作 Square,里面有个大神叫 Jake Wharton,开源了一个神奇的框架叫作 ButterKnife,这个框架虽然也采用了注解进行注入,不过人家但是编译期生成代码的方式,对运行时没有任何反作用,果然见效快,疗效好,只是编译期有一点点时间成本而已。app

说句题外话,现现在作 Android 若是不知道 Jake Wharton,我以为面试能够直接 Pass 掉了。。。哈哈,开玩笑啦

title

1.2 ButterKnife 怎么用?

怎么介绍一个东西,那真是一个折学问题。别老说我没文化,个人意思是比较曲折嘛。

咱们仍是要先简单介绍一些 ButterKnife 的基本用法,这些知识你在 ButterKnife 这里也能够看到。

简单来讲,使用 ButterKnife 须要三步走:

  1. 配置编译环境,因为 Butterknife 用到了注解处理器,因此,比起通常的框架,配置稍微多了些,不过也很简单啦:

    buildscript {
        repositories {
          mavenCentral()
        }
        dependencies {
          classpath 'com.android.tools.build:gradle:1.3.1'
          classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
        }
    }
    apply plugin: 'com.neenbedankt.android-apt'
    ...
    dependencies {
        compile 'com.jakewharton:butterknife:8.1.0'
        apt 'com.jakewharton:butterknife-compiler:8.1.0'
    }
  2. 用注解标注须要注解的对象,好比 View,好比一些事件方法(用做 onClick 之类的),例:

    @Bind(R.id.title)
    TextView title;
    
    @OnClick(R.id.hello)
    void sayHello() {
        Toast.makeText(this, "Hello, views!", LENGTH_SHORT).show();
        ButterKnife.apply(headerViews, ALPHA_FADE);
    }
  3. 在初始化布局以后,调用 bind 方法:

    setContentView(R.layout.activity_main);
    ButterKnife.bind(this);//必定要在 setContentView 以后哈,否则你就等着玩空指针吧

    瞧,这时候你要是编译一下,你的代码就能欢快的跑起来啦,什么 findViewById,什么 setOnClickListener,我历来没据说过~

哈,不过你仍是要当心一点儿,你要是有本事写成这样,ButterKnife 就说『信不信我报个错给你看啊!』

@Bind(R.id.title)
private TextView title;

@OnClick(R.id.hello)
private void sayHello() {
    Toast.makeText(this, "Hello, views!", LENGTH_SHORT).show();
    ButterKnife.apply(headerViews, ALPHA_FADE);
}
Error:(48, 22) error: @Bind fields must not be private or static. (com.example.butterknife.SimpleActivity.title)
Error:(68, 18) error: @OnClick methods must not be private or static. (com.example.butterknife.SimpleActivity.sayHello)

这又是为神马嘞?若是你知道 ButterKnife 的机制,那么这个问题就很清晰了,前面咱们已经提到,ButterKnife 是经过注解处理器来生成辅助代码进而达到本身的注入目的的,那么咱们就有必要瞅瞅它究竟生成了什么鬼。

话说,生成的代码就在 build/generated/source/apt 下面,咱们就以 ButterKnife 的官方 sample 为例,它生成的代码以下:

title

让咱们看一下 SimpleActivity$$ViewBinder:

public class SimpleActivity$$ViewBinder<T extends SimpleActivity> implements ViewBinder<T> {
  @Override
  public void bind(final Finder finder, final T target, Object source) {
    Unbinder unbinder = new Unbinder(target);
    View view;
    //注入 title,这里的 target 其实就是咱们的 Activity
    view = finder.findRequiredView(source, 2130968576, "field 'title'");
    target.title = finder.castView(view, 2130968576, "field 'title'");
    
    //下面注入 hello 这个 Button,并为其设置 click 事件
    view = finder.findRequiredView(source, 2130968578, "field 'hello', method 'sayHello', and method 'sayGetOffMe'");
    target.hello = finder.castView(view, 2130968578, "field 'hello'");
    unbinder.view2130968578 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.sayHello();
      }
    });
    ...
  }

  ...
}

咱们看到这里面有个叫 bind 的方法,这个方法跟咱们以前调用的 ButterKnife.bind的关系可想而知——其实,后者只是个引子,调用它就是为了调用生成的代码。什么,不信?好吧,我就喜欢大家这些充满好奇的娃。咱们在调用 ButterKnife.bind 以后,会进入下面的方法:

static void bind(@NonNull Object target, @NonNull Object source, @NonNull Finder finder) {
    Class<?> targetClass = target.getClass();
    try {
      ViewBinder<Object> viewBinder = findViewBinderForClass(targetClass);
      viewBinder.bind(finder, target, source);
    } catch (Exception e) {
      //省略异常处理
    }
  }

咱们知道参数 targetsource 在这里都是我们的 Activity 的实例,那么找到的 viewBinder 又是什么鬼呢?

private static ViewBinder<Object> findViewBinderForClass(Class<?> cls)
      throws IllegalAccessException, InstantiationException {
    ViewBinder<Object> viewBinder = BINDERS.get(cls);
    //先找缓存
    if (viewBinder != null) {
      return viewBinder;
    }
    //检查下是否支持这个类
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      return NOP_VIEW_BINDER;
    }
    try {
      //找到类名为 Activity 的类名加 "$$ViewBinder" 的类,实例化,并返回
      Class<?> viewBindingClass = Class.forName(clsName + "$$ViewBinder");
      //noinspection unchecked
      viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
    } catch (ClassNotFoundException e) {
      //注意这里支持了继承关系
      viewBinder = findViewBinderForClass(cls.getSuperclass());
    }
    //缓存 viewBinder
    BINDERS.put(cls, viewBinder);
    return viewBinder;
  }

简单看下注释就很容易理解了,若是咱们的 Activity 名为 SimpleActivity,那么找到的 ViewBinder 应该就是 SimpleActivity$$ViewBinder

仍是回到咱们前面的问题,若是须要注入的成员是 private,ButterKnife 会报错,显然,若是 titleprivate,生成的代码中又写到 target.title,这不就是在搞笑么?小样儿,你觉得你是生成的代码, Java 虚拟机就会让你看见不应看的东西么?

固然,对须要注入的成员的要求不止这些啦,咱们稍后就会知道,其实对于静态成员和某些特定包下的类的成员也是不支持注入的。

1.3 小结

这个框架给咱们的感受就是,用起来炒鸡简单有木有。说话想当年,@ 给了咱们上网冲浪的感受,如今,咱们仍然只须要在代码里面 @ 几下,就能够在后面各类浪了。

等等,这么简单的表象后面,究竟隐藏着怎样的秘密?它那光鲜的外表下面又有那些不可告人的故事?请看下回分解。

二、ButterKnife,给我上一盘蛋炒饭

title

Jake 大神,我赌一个月好莱坞会员,你必定是一个吃货。。

咱们把生成代码这个过程比做一次蛋炒饭,在炒的时候你要先准备炊具,接着准备用料,而后开炒,出锅。

2.1 准备炊具

蛋炒饭是在锅里面炒出来的,那么咱们的 ButterKnife 的"锅"又是什么鬼?

闲话少叙,且说从咱们配置好的注解,到最终生成的代码,这是个怎样的过程呢?

title

上图很清晰嘛,虽然什么都没说。额。。别动手。。

你看图里面 ButterKnife 很厉害的样子,其实丫是仗势欺人。仗谁的势呢?咱们千呼万唤始出来滴注解处理器,这时候就要登上历史舞台啦!

话说 Java 编译器编译代码以前要先来个预处理,这时候编译器会对 classpath 下面有下图所示配置的注解处理器进行调用,那么这时候咱们就能够干坏事儿了(怎么每到这个时候都会很兴奋呢。。)

title

因此,若是你要本身写注解处理器的话,首先要继承 AbstractProcessor ,而后写下相似的配置。不过稍等一下,让咱们看下 Butterknife 是怎么作的:

@AutoService(Processor.class)
public final class ButterKnifeProcessor extends AbstractProcessor {
    ...
}

AutoService 是干什么的呢?看看刚才的图,有没有注意到那个文件夹是红色?是的,它是自动生成的,而负责生成这个配置的家伙就是 AutoService,这是 google 的一个开源组件,很是简单,我就很少说了。

简而言之:注解处理器为咱们打开了一扇门,让咱们能够在 Java 编译器编译代码以前,执行一段咱们的代码。固然这代码也不必定就是要生成别的代码了,你能够去检查那些被注解标注的代码的命名是否规范(周志明大神的 《深刻理解 Java 虚拟机》一书当中有这个例子)。啊,你说你要去输出一个 “Hello World”,~~(╯﹏╰)b 也能够。。吧。。

2.2 嘿蛋炒饭,最简单又最困难

既然知道了程序的入口,那么咱们就要来看看 ButterKnife 究竟干了什么见不得人的事儿。在这里,全部的输入就是咱们在本身的代码中配置的注解,全部的输出,就是生成的用于注入对象的辅助代码。

关于注解处理器的更多细节请你们参考相应的资料哈,我这里直接给出 ButterKnife 的核心代码,在 ButterKnifeProcessor.process 当中:

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //下面这一句解析注解
    Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);
    
    //解析完成之后,须要生成的代码结构已经都有了,它们都存在于每个 BindingClass 当中
    for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingClass bindingClass = entry.getValue();

      try {
        //这一步完成真正的代码生成
        bindingClass.brewJava().writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
            e.getMessage());
      }
    }

    return true;
  }

咱们知道,ButterKnife 对于须要注入对象的成员有要求的,在解析注解配置时,首先要对被标注的成员进行检查,若是检查失败,直接抛异常。

title

在分析解析过程时,咱们以 @Bind 为例,注解处理器找到用 @Bind 标注的成员,检验这些成员是否符合注入的条件(好比不能是 private,不能是 static 之类),以后将注解当中的值取出来,建立或者更新对应的 BindingClass

private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
    Set<String> erasedTargetNames = new LinkedHashSet<>();
    
    // Process each @Bind element.
    for (Element element : env.getElementsAnnotatedWith(Bind.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        parseBind(element, targetClassMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, Bind.class, e);
      }
    }
    ...
    return targetClassMap;
  }

如今之前面提到的 title 为例,解析的时候拿到的 element 其实对应的就是 title 这个变量。

private void parseBind(Element element, Map<TypeElement, BindingClass> targetClassMap,
      Set<String> erasedTargetNames) {

    ... 省略掉检验 element 是否符合条件的代码 ...
    
    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.ARRAY) {
      parseBindMany(element, targetClassMap, erasedTargetNames);
    } else if (LIST_TYPE.equals(doubleErasure(elementType))) {
      parseBindMany(element, targetClassMap, erasedTargetNames);
    } else if (isSubtypeOfType(elementType, ITERABLE_TYPE)) {
    // 显然这里被注入的对象类型不能是 Iterable,List 除外~
      error(element, "@%s must be a List or array. (%s.%s)", Bind.class.getSimpleName(),
          ((TypeElement) element.getEnclosingElement()).getQualifiedName(),
          element.getSimpleName());
    } else {
      parseBindOne(element, targetClassMap, erasedTargetNames);
    }
  }

在注入 title 时,对应的要接着执行 parseBindOne 方法:

private void parseBindOne(Element element, Map<TypeElement, BindingClass> targetClassMap,
      Set<String> erasedTargetNames) {
    ... 省略掉一些校验代码 ...
    
    if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
        ... 处理错误,显然被注入的必须是 View 的子类 ...
    }

    // Assemble information on the field.
    int[] ids = element.getAnnotation(Bind.class).value();
    if (ids.length != 1) {
       ... 之前已经确认是单值绑定,因此出现了参数为多个的状况就报错...
    }

    ... 省略构建 BindingClass 对象的代码 ...
    BindingClass bindingClass = targetClassMap.get(enclosingElement);

    String name = element.getSimpleName().toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);

    // 根据注解信息来生成注入关系,并添加到 bindingClass 当中
    FieldViewBinding binding = new FieldViewBinding(name, type, required);
    bindingClass.addField(id, binding);

    ...
  }

其实每个注解的解析流程都是相似的,解析的最终目标就是在这个 bindingClassaddField,这意味着什么呢?

经过前面的分析,其实咱们已经知道解析注解的最终目标是生成那些用于注入的代码,这就比如咱们让注解管理器写代码。这彷佛是一个颇有意思的话题,若是你的程序足够聪明,它就能够本身写代码~~

那么这么说 addField 就是要给生成的代码添加一个属性咯?不不不,是添加一组注入关系,后面生成代码时,注解管理器就须要根据这些解析来的关系来组织生成的代码。因此,要不要再看一下生成的代码,看看还有没有新的发现?

2.三、出锅咯

话说,注解配置已经解析完毕,咱们已经知道咱们要生成的代码长啥样了,那么下一个问题就是如何真正的生成代码。这里用到了一个工具 JavaPoet,一样出自 Square 的大神之手。JavaPoet 提供了很是强大的代码生成功能,好比咱们下面将给出生成输出 HelloWorld 的 JavaDemo 的代码:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)
    .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);

这样就能够生成下面的代码了:

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

其实咱们本身写个程序生成一些代码并不难,不过导包这个事情却很是的使人焦灼,别担忧,JavaPoet 能够把这些通通搞定。

有了个简单的认识以后,咱们要看下 ButterKnife 都用 JavaPoet 干了什么。还记得下面的代码么:

bindingClass.brewJava().writeTo(filer);

这句代码将 brew 出来的什么鬼东西写到了 filer 当中,Filer 嘛发挥想象力就知道是相似于文件的东西,换句话说,这句代码就是完成代码生成到指定文件的过程。

Brew Java !!

~ ~ ~ heating ~ ~ ~
=> => pumping => =>
[ ]P coffee! [ ]P

JavaFile brewJava() {
    TypeSpec.Builder result = TypeSpec.classBuilder(className)
    //添加修饰符为 public,生成的类是 public 的
        .addModifiers(PUBLIC)
        .addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));

    /*其实 Bind 过程也是有继承关系的,我有一个 Activity A 有注入,另外一个 B 继承它,那么生成注入 B 的成员的代码时,就要把 A 的注入一块儿捎上*/
    if (parentViewBinder != null) {
      result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentViewBinder),
          TypeVariableName.get("T")));
    } else {
      result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T")));
    }

    if (hasUnbinder()) {
      result.addType(createUnbinderClass());
    }

    //这一句很关键,咱们的绝大多数注入用到的代码都在这里了
    result.addMethod(createBindMethod());

    //输出一个 JavaFile 对象(其实这里离生成最终的代码已经很近了),完工
    return JavaFile.builder(classPackage, result.build())
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  }

如今咱们须要继续看下 createBindMethod 方法,这个方法是生成代码的关键~

private MethodSpec createBindMethod() {
    /*建立了一个叫作 bind 的方法,添加了 @Override 注解,方法可见性为 public
     以及一些参数类型 */
    MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
        .addAnnotation(Override.class)
        .addModifiers(PUBLIC)
        .addParameter(FINDER, "finder", FINAL)
        .addParameter(TypeVariableName.get("T"), "target", FINAL)
        .addParameter(Object.class, "source");

    if (hasResourceBindings()) {
      // Aapt can change IDs out from underneath us, just suppress since all will work at runtime.
      result.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
          .addMember("value", "$S", "ResourceType")
          .build());
    }
    
    // Emit a call to the superclass binder, if any.
    if (parentViewBinder != null) {
      result.addStatement("super.bind(finder, target, source)");
    }
    
    /* 关于 unbinder,咱们一直都没有提到过,若是咱们有下面的注入配置:
        @Unbinder
        ButterKnife.Unbinder unbinder;
    * 那么这时候就会在生成的代码中添加下面的代码,这实际上就是构造 unbinder
    */
    // If the caller requested an unbinder, we need to create an instance of it.
    if (hasUnbinder()) {
      result.addStatement("$T unbinder = new $T($N)", unbinderBinding.getUnbinderClassName(),
          unbinderBinding.getUnbinderClassName(), "target");
    }


    /*
    * 这里就是注入 view了,addViewBindings 这个方法其实就生成功能上相似
        TextView textView = (TextView) findViewById(...) 的代码
    */
    if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) {
      // Local variable in which all views will be temporarily stored.
      result.addStatement("$T view", VIEW);

      // Loop over each view bindings and emit it.
      for (ViewBindings bindings : viewIdMap.values()) {
        addViewBindings(result, bindings);
      }

      // Loop over each collection binding and emit it.
      for (Map.Entry<FieldCollectionViewBinding, int[]> entry : collectionBindings.entrySet()) {
        emitCollectionBinding(result, entry.getKey(), entry.getValue());
      }
    }

    /*
    * 注入 unbinder
    */ 
    // Bind unbinder if was requested.
    if (hasUnbinder()) {
      result.addStatement("target.$L = unbinder", unbinderBinding.getUnbinderFieldName());
    }

    /* ButterKnife 其实不止支持注入 View, 还支持注入 字符串,主题,图片。。
    * 全部资源里面你能想象到的东西
    */
    if (hasResourceBindings()) {
        //篇幅有限,我仍是省略掉他们吧
        ...
    }

    return result.build();
  }

不知道为何,这段代码让我想起了我写代码的样子。。那分明就是 ButterKnife 在替咱们写代码嘛。

固然,这只是生成的代码中最重要的最核心的部分,为了方便理解,我把 demo 里面生成的这个方法列出来方便查看:

@Override
  public void bind(final Finder finder, final T target, Object source) {
    //构造 unbinder
    Unbinder unbinder = new Unbinder(target);
    //下面开始 注入 view
    View view;
    view = finder.findRequiredView(source, 2130968576, "field 'title'");
    target.title = finder.castView(view, 2130968576, "field 'title'");
    //... 省略掉其余成员的注入 ...
    //注入 unbinder
    target.unbinder = unbinder;
  }

三、Hack 一下,定义咱们本身的注解 BindLayout

我一直以为,既然 View 都能注入了,咱能不能把 layout 也注入了呢?显然这没什么难度嘛,可为啥 Jake 大神没有作这个功能呢?我以为主要是由于。。。你想哈,你注入个 layout,大概要这么写

@BindLayout(R.layout.main)
public class AnyActivity extends Activity{...}

可咱们平时怎么写呢?

public class AnyActivity extends Activity{
    @Override
    protected void onCreate(Bundle savedInstances){
        super.onCreate(savedInstances);
        setContentView(R.layout.main);
    }
}

你别说你不继承 onCreate 方法啊,因此好像始终要写一句,性价比不高?谁知道呢。。。

不过呢,我们接下来就运用咱们的神功,给 ButterKnife 添砖加瓦(这怎么感受像校长说的呢。。嗯,他说的是社河会蟹主@义),让 ButterKnife 能够 @BindLayout。先看效果:

//注入 layout
@BindLayout(R.layout.simple_activity)
public class SimpleActivity extends Activity {
    ...
}

生成的代码:

public class SimpleActivity$$ViewBinder<T extends SimpleActivity> implements ViewBinder<T> {
  @Override
  public void bind(final Finder finder, final T target, Object source) {
    //生成了这句代码来注入 layout
    target.setContentView(2130837504);
    //下面省略掉的代码咱们已经见过啦,就是注入 unbinder,注入 view
    ...
  }
  
  ...
}

那么咱们要怎么作呢?一个字,顺藤摸瓜~

第一步,固然是要定义注解 BindLayout

@Retention(CLASS) @Target(TYPE)
public @interface BindLayout {
    @LayoutRes int value();
}

第二步,咱们要去注解处理器里面添加对这个注解的支持:

@Override public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<>();
    ...
    types.add(BindLayout.class.getCanonicalName());
    ...
    return types;
  }

第三步,注解处理器的解析环节要添加支持:

private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();
    Set<String> erasedTargetNames = new LinkedHashSet<>();

    // Process each @Bind element.
    for (Element element : env.getElementsAnnotatedWith(BindLayout.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
          parseBindLayout(element, targetClassMap, erasedTargetNames);
      } catch (Exception e) {
          logParsingError(element, BindLayout.class, e);
      }
    }
    ...
}

下面是 parseBindLayout 方法:

private void parseBindLayout(Element element, Map<TypeElement, BindingClass> targetClassMap, Set<String> erasedTargetNames) {
    /*与其余注解解析不一样,BindLayout 标注的类型就是 TYPE,因此这里直接强转为 
     TypeElement,其实就是对应于 Activity 的类型*/
    TypeElement typeElement = (TypeElement) element;
    Set<Modifier> modifiers = element.getModifiers();
    
    // 只有 private 不能够访问到,static 类型不影响,这也是与其余注解不一样的地方
    if (modifiers.contains(PRIVATE)) {
        error(element, "@%s %s must not be private. (%s.%s)",
                BindLayout.class.getSimpleName(), "types", typeElement.getQualifiedName(),
                element.getSimpleName());
        return;
    }
    
    // 一样的,对于 android 开头的包内的类不予支持
    String qualifiedName = typeElement.getQualifiedName().toString();
    if (qualifiedName.startsWith("android.")) {
        error(element, "@%s-annotated class incorrectly in Android framework package. (%s)",
                BindLayout.class.getSimpleName(), qualifiedName);
        return;
    }
    
    // 一样的,对于 java 开头的包内的类不予支持
    if (qualifiedName.startsWith("java.")) {
        error(element, "@%s-annotated class incorrectly in Java framework package. (%s)",
                BindLayout.class.getSimpleName(), qualifiedName);
        return;
    }

    /* 咱们暂时只支持 Activity,若是你想支持 Fragment,须要区别对待哈,
    由于两者初始化 View 的代码不同 */
    if(!isSubtypeOfType(typeElement.asType(), ACTIVITY_TYPE)){
        error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
                BindLayout.class.getSimpleName(), typeElement.getQualifiedName(), element.getSimpleName());
        return;
    }
    
    // 拿到注解传入的值,好比 R.layout.main
    int layoutId = typeElement.getAnnotation(BindLayout.class).value();
    if(layoutId == 0){
        error(element, "@%s for a Activity must specify one layout ID. Found: %s. (%s.%s)",
                BindLayout.class.getSimpleName(), layoutId, typeElement.getQualifiedName(),
                element.getSimpleName());
        return;
    }
    
    BindingClass bindingClass = targetClassMap.get(typeElement);
    if (bindingClass == null) {
        bindingClass = getOrCreateTargetClass(targetClassMap, typeElement);
    }
    
    // 把这个布局的值塞给 bindingClass,这里我只是简单的存了下这个值
    bindingClass.setContentLayoutId(layoutId);
    log(element, "element:" + element + "; targetMap:" + targetClassMap + "; erasedNames: " + erasedTargetNames);
}

第四步,添加相应的生成代码的支持,这个在 BindingClass.createBindMethod 当中:

private MethodSpec createBindMethod() {
    MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
        .addAnnotation(Override.class)
        .addModifiers(PUBLIC)
        .addParameter(FINDER, "finder", FINAL)
        .addParameter(TypeVariableName.get("T"), "target", FINAL)
        .addParameter(Object.class, "source");

    
    if (hasResourceBindings()) {
        ... 省略之 ...
    }
    
    //若是 layoutId 不为 0 ,那说明有绑定,添加一句 setContentView 完事儿~~
    //要注意的是,这句要比 view 注入在前面。。。你懂的,否则本身去玩空指针
    if(layoutId != 0){
      result.addStatement("target.setContentView($L)", layoutId);
    }

    ...
}

这样,咱们就能够告别 setContentView 了,写个注解,很是清爽,随意打开个 Activity 一眼就看到了布局在哪里,哈哈哈哈哈

title

实际上是说你胖。。

四、androidannotations 和 dagger2

4.1 androidannotations

androidannotations 一样是一个注入工具,若是你稍微接触一下它,你就会发现它的原理与 ButterKnife 一模一样。下面咱们给出其中很是核心的代码:

private void processThrowing(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) throws Exception {
        if (nothingToDo(annotations, roundEnv)) {
            return;
        }

        AnnotationElementsHolder extractedModel = extractAnnotations(annotations, roundEnv);
        AnnotationElementsHolder validatingHolder = extractedModel.validatingHolder();
        androidAnnotationsEnv.setValidatedElements(validatingHolder);

        try {
            AndroidManifest androidManifest = extractAndroidManifest();
            LOGGER.info("AndroidManifest.xml found: {}", androidManifest);

            IRClass rClass = findRClasses(androidManifest);

            androidAnnotationsEnv.setAndroidEnvironment(rClass, androidManifest);

        } catch (Exception e) {
            return;
        }

        AnnotationElements validatedModel = validateAnnotations(extractedModel, validatingHolder);

        ModelProcessor.ProcessResult processResult = processAnnotations(validatedModel);

        generateSources(processResult);
    }

咱们就简单看下,其实也是注解解析和代码生成几个步骤,固然,因为 androidannotations 支持的功能要复杂的多,不只仅包含 UI 注入,还包含线程切换,网络请求等等,所以它的注解解析逻辑也要复杂得多,阅读它的源码时,建议多多关注一下它的代码结构设计,很是不错。

从使用的角度来讲,ButterKnife 只是针对 UI 进行注入,功能比较单一,而 androidannotations 真是有些庞大和强大,究竟使用哪个框架,那要看具体需求了。

4.2 Dagger 2

Dagger 2 算是超级富二代了,妈是 Square,爹是 Google—— Dagger 2 源自于 Square 的开源项目,目前已经由 Google 接管(怎么感受 Google 喜当爹的节奏 →_→)。

Dagger 本是一把利刃,它也是用来注入成员的一个框架,不过相对于前面的两个框架,它

  • 显得更基础,由于它不针对具体业务

  • 显得更通用,由于它不依赖运行平台

  • 显得更复杂,由于它更关注于对象间的依赖关系

用它的开发者说的一句话就是(大意):有一天,咱们发现咱们的构造方法竟然须要 3000 行,这时候咱们意识到是时候写一个框架帮咱们完成构造方法了。

换句话说,若是你的构造方法没有那么长,其实也不必引入 Dagger 2,由于那样会让你的代码显得。。。不是那么的好懂。

固然,咱们放到这里提一下 Dagger 2,是由于它 彻底去反射,实现的思想与前面提到的两个框架也是一毛同样啊。因此你能够不假思索的说,Dagger 2 确定至少有两个模块,一个是 compiler,里面有个注解处理器;还有一个是运行时须要依赖的模块,主要提供 Dagger 2 的注解支持等等。

五、小结

本文经过对 ButterKnife 的源码的分析,咱们了解到了 ButterKnife 这样的注入框架的实现原理,同时咱们也对 Java 的注解处理机制有了必定的认识;接着咱们还对 ButterKnife 进行了扩充的简单尝试——总而言之,使用起来很是简单的 ButterKnife 框架的实现实际上涉及了较多的知识点,这些知识点相对生僻,却又很是的强大,咱们能够利用这些特性来实现各类各样个性化的需求,让咱们的工做效率进一步提升。

来吧,解放咱们的双手!

更多精彩内容欢迎关注bugly的微信公众帐号:

相关文章
相关标签/搜索