学习笔记ButterKnife

深度学一下Butterknife是如何帮我们自动生成findViewById的
第一行

ButterKnife.bind(this);

点开

public static Unbinder bind(@NonNull Activity target) {
  View sourceView = target.getWindow().getDecorView();
  return bind(target, sourceView);
}

获取 DecorViewDecorView 包含setContentView 的xml和 ActionBarDecorView 又被 PhoneWindow 包含。

@NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    //获取Class对象
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    //获取constructor对象
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

    if (constructor == null) {
      return Unbinder.EMPTY;
    }

    try {
      //返回生成的对象
      return constructor.newInstance(target, source);
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } //去掉了部分catch
  }

Class类:在java中,每个定义的类都会有一个相应的 Class 。在编译完成后生成在 .class 文件中,它是一个类,在long包中,私有构造函数,像基本类型、数组、void也都有一个 Class 是用来表示这个类的类型信息的。Java在运行时,系统会对所有对象进行 运行时类型标识,虚拟机运行时会以准确的方法执行。
Class:获取

  1. 获取Class.forName(“类的全限定名”)
  2. 实例对象.getClass()
  3. 类名.class (类字面常量)
    使用
  4. Class.forName("类名")装入类,只做静态初始化,返回Class
  5. newInstance()调用无惨构造函数返回实例
  6. isInstance(..)判断里面的对象是否是子类
  7. isInterface(..)判断接口
  8. getSimpleName()获取对象类名等等,都是和当前类相关的内容
    类名.classClass 类的区别:
    类名.class是 JVM将类装入内存 (还未装入内存),不做初始化工作,返回Class对象。只能用类名点出class,Button.class
    Constructor(构造器)类:提供有关类 的构造函数,比如String.class未被装入内存,可以直接获取他的构造函数
Constructor<?>[] c = String.class.getConstructors();

之后可以通过对象 c 找到需要的构造函数进行 newInstance(..) 实例化

findBindingConstructorForClass()
 static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();

@Nullable @CheckResult @UiThread
 private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
   //这里做了一个缓存,如果缓存有直接返回
   Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
   if (bindingCtor != null || BINDINGS.containsKey(cls)) {
     if (debug) Log.d(TAG, "HIT: Cached in binding map.");
     return bindingCtor;
   }
   //获取类名
   String clsName = cls.getName();
   if (clsName.startsWith("android.") || clsName.startsWith("java.")
       || clsName.startsWith("androidx.")) {
     if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
     return null;
   }
   try {
     //根据 类名_ViewBinding  返回Class对象  
     Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
     //根据Class对象  返回 构造函数类 对象
     bindingCtor = (Constructor<? extends Unbinder>)bindingClass.getConstructor(cls, View.class);
     if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
   } catch (ClassNotFoundException e) {
     if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
     bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
   } catch (NoSuchMethodException e) {
     throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
   }
   //放入缓存,便于重用
   BINDINGS.put(cls, bindingCtor);
   return bindingCtor;
 }

根据类名_ViewBinding返回Class对象,比如你的一个界面叫 MainActivity ,注解生成器会生成一个类文件叫 MainActivity_ViewBinding。
ButterKnife类主要功能就是,运行注解处理器 生成出来的文件。
关键还是在于ButterKnife的注解处理类

ButterKnifeProcessor extends AbstractProcessor

Java注解处理器,简单来说就是在编译时,扫描所有文件,收集带有注解的变量、类方法等等统一到一块,主要是 编译时

public final class ButterKnifeProcessor extends AbstractProcessor {
  //变量名
  //定义了一些字符串 用private static final String修饰
  //定义了List放的是一些 事件 的 注解集合
  @Override public synchronized void init(ProcessingEnvironment env) {
      //初始化一些内容
  }

  //支持的注解类型 意思就是 添加了 才能用
  @Override public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<>();
    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
      types.add(annotation.getCanonicalName());
    }
    return types;
  }

  private Set<Class<? extends Annotation>> getSupportedAnnotations() {
    Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();

    annotations.add(BindAnim.class);
    annotations.add(BindArray.class);
    annotations.add(BindBitmap.class);
    annotations.add(BindBool.class);
    annotations.add(BindColor.class);
    annotations.add(BindDimen.class);
    annotations.add(BindDrawable.class);
    annotations.add(BindFloat.class);
    annotations.add(BindFont.class);
    annotations.add(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);   //这里是所有的事件注解

    return annotations;
  }
  //生成代码的部分
  @Override public boolean process(Set<? extends TypeElement> elements,   RoundEnvironment env) {
    //寻找解析注解
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    //遍历上面的map 循环的生成文件
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();
      //生成JavaFile对象
      JavaFile javaFile = binding.brewJava(sdk, debuggable);
      try {
        //生成文件
        javaFile.writeTo(filer);
        } catch (IOException e) {
          error(typeElement, "Unable to write binding for type %s: %s", typeElement,  e.getMessage());
        }
      }

      return false;
    }
}

bindingMap:寻找注解,以Key-value的方式存储
key:TypeElement类型元素,大概包含包名、类名、文件路径等等,我理解为“类信息”
value:BindingSet是一个所有绑定的集合,也就是 “类” 里面的有 需要绑定的集合
下面是里面代码

1、findAndParseTargets(env)
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    //这个不是最后返回的对象,这个只存了BindAnim、BindString、BindView等等的数据
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

    //隐藏代码@BindAnim
    //影藏很多 bind
    //隐藏代码@BindString

    //绑定  界面上的View
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      // we don't SuperficialValidation.validateElement(element)
      // so that an unresolved View type can be generated by later processing rounds
      try {
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }
    //隐藏代码@BindViews

    //隐藏代码 bindlistener.


    Map<TypeElement, ClasspathBindingSet> classpathBindings =
        findAllSupertypeBindings(builderMap, erasedTargetNames);

    //使用队列的方式,绑定superClass
    // Associate superclass binders with their subclass binders. This is a queue-based tree walk
    // which starts at the roots (superclasses) and walks to the leafs (subclasses).
    Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
        new ArrayDeque<>(builderMap.entrySet());
    Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
    while (!entries.isEmpty()) {
      Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();

      TypeElement type = entry.getKey();
      BindingSet.Builder builder = entry.getValue();

      TypeElement parentType = findParentType(type, erasedTargetNames, classpathBindings.keySet());
      if (parentType == null) {
        bindingMap.put(type, builder.build());
      } else {
        BindingInformationProvider parentBinding = bindingMap.get(parentType);
        if (parentBinding == null) {
          parentBinding = classpathBindings.get(parentType);
        }
        if (parentBinding != null) {
          builder.setParent(parentBinding);
          bindingMap.put(type, builder.build());
        } else {
          // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
          entries.addLast(entry);
        }
      }
    }

    return bindingMap;
  }

这里分2块内容
第一部分是查找,BindString、BindView等等 的类
第二部分是把所有的类进行联合变成

第一部分
//关键是这里BindView, 先不看BindString,BindAnim等等 都是相通的
parseBindView(element, builderMap, erasedTargetNames);

//直接贴代码
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
    Set<TypeElement> erasedTargetNames) {
  TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

  //校验类型
  //方法名意思是:生成后的代码 无法访问一些内容,比如private等等
  //isBindingInWrongPackage:绑定的内容 在错误的包
  boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
      || isBindingInWrongPackage(BindView.class, element);

  // 验证类型是否从View扩展,就是是否是View的子类
  TypeMirror elementType = element.asType();
  //判断是否是变量
  if (elementType.getKind() == TypeKind.TYPEVAR) {
    TypeVariable typeVariable = (TypeVariable) elementType;
    elementType = typeVariable.getUpperBound();
  }
  Name qualifiedName = enclosingElement.getQualifiedName();
  Name simpleName = element.getSimpleName();
  //判断是不是View类型,是不是接口
  if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
    if (elementType.getKind() == TypeKind.ERROR) {
      note(element, "@%s field with unresolved type (%s) "
              + "must elsewhere be generated as a View or interface. (%s.%s)",
          BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
    } else {
      error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
          BindView.class.getSimpleName(), qualifiedName, simpleName);
      hasError = true;
    }
  }
  //有错误返回
  if (hasError) {
    return;
  }

  //开始收集所有信息
  //获取id
  int id = element.getAnnotation(BindView.class).value();
  //获取一个builder,这个builder是从builderMap中取的,这个builderMap是传进来的
  //参数enclosingElement 是这样赋值的 (TypeElement) element.getEnclosingElement();
  //这个TypeElement  我之前写了是 理解为 “类信息” 的
  //所以是用get方法取, 因为第一个绑定方法是BindAnim,有可能这里已经 创建过了
  BindingSet.Builder builder = builderMap.get(enclosingElement);
  Id resourceId = elementToId(element, BindView.class, id);
  //判断builder是否为空
  if (builder != null) {
    //不空,在查找是否存在了,存在就是重复绑定了 打印异常 返回
    String existingBindingName = builder.findExistingBindingName(resourceId);
    if (existingBindingName != null) {
      error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
          BindView.class.getSimpleName(), id, existingBindingName,
          enclosingElement.getQualifiedName(), element.getSimpleName());
      return;
    }
  } else {
    //为空 则生成一个Builder出来
    builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
  }
  //获取名字、类型
  String name = simpleName.toString();
  TypeName type = TypeName.get(elementType);
  //是否有 Nullable 标签
  boolean required = isFieldRequired(element);
  //在builder中添加一个Field ,主要是FieldViewBinding,如果是@BindDrawable则会用FieldDrawableBinding类
  builder.addField(resourceId, new FieldViewBinding(name, type, required));

  //最后添加到set中,erasedTargetNames这个对象主要的作用是在后面
  erasedTargetNames.add(enclosingElement);
}

这里也不是很复杂,也就是根据注解获取到的内容,判断变量是不是View的子类、不是static、不是private等等,然后获取名字、Id,然后用 FieldViewBinding 类包装一下
到这里其实已经根据所有有注解的内容已经按照 “类” 分好了,分好还不行,肯定会有什么BaseActivity、BaseDialog、BaseView所以要整合,让生成的类有联系

第二部分
Map<TypeElement, ClasspathBindingSet> classpathBindings =
        findAllSupertypeBindings(builderMap, erasedTargetNames);
//组合所有类的关系  组成 树
//这里注释也写了,用队列的方式,将超类与子类绑定,从根开始
// Associate superclass binders with their subclass binders. This is a queue-based tree walk
// which starts at the roots (superclasses) and walks to the leafs (subclasses).
//这个获取的所有的“类”  放入了队列
Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
        new ArrayDeque<>(builderMap.entrySet());
//即将返回的对象
Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
while (!entries.isEmpty()) {
  Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();

  TypeElement type = entry.getKey();
  BindingSet.Builder builder = entry.getValue();
  //拿队列出来的第一个 在erasedTargetNames中查询,父类是否在这个这个集合里
  //为什么要这样? 因为可能 你在父类中 绑定了一个String
  //在子类中使用了这个String,所以必须先初始化父类的String
  //翻看生成好的代码来看,初始化都是在构造函数中进行绑定的
  //所以考虑继承情况,必须把所有的类进行一个关联。
  TypeElement parentType = findParentType(type, erasedTargetNames, classpathBindings.keySet());
  if (parentType == null) {
    //如果没有父类,则直接 放入
    bindingMap.put(type, builder.build());
  } else {
    //如果父类有绑定
    //再从bindingMap(即将返回的对象)中取看看 是否已经放进去了
    BindingInformationProvider parentBinding = bindingMap.get(parentType);
    if (parentBinding == null) {
      //如果没绑定进去 再从classpathBindings中取 一个父类
      parentBinding = classpathBindings.get(parentType);
    }
    if (parentBinding != null) {
      //如果这个父类不是空的 则和当前循环里的builder  子、父类绑定
      builder.setParent(parentBinding);
      //放入即将返回的map里
      bindingMap.put(type, builder.build());
    } else {
      //翻译是:有个超类绑定,但还没有构建它,放到后面继续排队
      // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
      //比如:子类继承父类都有绑定,这时候子类需要关联父类,父类还没初始化,
      //把子类放到队尾,等父类初始化完成在进行关联
      entries.addLast(entry);
    }
  }
}
//返回,这时候这个map的key 就全是“类信息”
//value就是获取好的 当前类里面需要绑定的内容
//并且 “类” 也已经做好了继承关系
return bindingMap;

看着代码挺多,主要就2个部分
以View为例,收集View信息,规制到同一个类中,把有关联的类,关联起来

2、binding.brewJava(sdk, debuggable)

到这里就简单多了

 Map<TypeElement, BindingSet> bindingMap =

照着这个bindingMap对象 遍历里面的“类”,生成文件即可
这里面用了另一个库 javapoet 专门用来生成类文件的

JavaFile brewJava(int sdk, boolean debuggable) {
  TypeSpec bindingConfiguration = createType(sdk, debuggable);
  return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration)
      .addFileComment("Generated code from Butter Knife. Do not modify!")
      .build();
}
//这个方法
createType(sdk, debuggable)

private TypeSpec createType(int sdk, boolean debuggable) {
  TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
      .addModifiers(PUBLIC)
      .addOriginatingElement(enclosingElement);
  if (isFinal) {
    result.addModifiers(FINAL);
  }

  if (parentBinding != null) {
    result.superclass(parentBinding.getBindingClassName());
  } else {
    result.addSuperinterface(UNBINDER);
  }

  if (hasTargetField()) {
    result.addField(targetTypeName, "target", PRIVATE);
  }

  if (isView) {
    result.addMethod(createBindingConstructorForView());
  } else if (isActivity) {
    result.addMethod(createBindingConstructorForActivity());
  } else if (isDialog) {
    result.addMethod(createBindingConstructorForDialog());
  }
  if (!constructorNeedsView()) {
    // Add a delegating constructor with a target type + view signature for reflective use.
    result.addMethod(createBindingViewDelegateConstructor());
  }
  result.addMethod(createBindingConstructor(sdk, debuggable));

  if (hasViewBindings() || parentBinding == null) {
    result.addMethod(createBindingUnbindMethod(result));
  }

  return result.build();
}

这里就不加注释了,这 PUBLIC、FINAL
下面的判断isView、isActivity、isDialog,点一个进去

private MethodSpec createBindingConstructorForActivity() {
  MethodSpec.Builder builder = MethodSpec.constructorBuilder()
      .addAnnotation(UI_THREAD)
      .addModifiers(PUBLIC)
      .addParameter(targetTypeName, "target");
  if (constructorNeedsView()) {
    builder.addStatement("this(target, target.getWindow().getDecorView())");
  } else {
    builder.addStatement("this(target, target)");
  }
  return builder.build();
}

这些 builder.addStatement 的内容,打开变异后的文件,都能看到
这里仅仅是生成文件的一些工具方法
最后就是 JavaFile.builder 建造者返回一个 build 对象
最后 javaFile.writeTo(filer); 生成文件

BindingSet

一个比较关键的类,是一个类里面,需要绑定的所有内容,比如String、View、Drawable等
代码太多,我就贴部分内容
一开始会有很多的 static final ClassName 声明的内容,ClassNamejavapoet 里面的,都是生成文件需要的包名
成员变量:

//隐藏了看变量名就能看懂的 变量
private final ImmutableList<ViewBinding> viewBindings;
private final ImmutableList<FieldCollectionViewBinding> collectionBindings;
private final ImmutableList<ResourceBinding> resourceBindings;
private final @Nullable BindingInformationProvider parentBinding;

ImmutableList:这是JDK提供的一些集合实现,当你不希望修改一个集合类,或者想做一个常量集合类可以使用它,当list用就行。
这里分成了3种情况进行绑定,BindViewBindViewsbindResource

BindView

绑定一个View ,ViewBinding 类是一个view的信息,生成 R文件 的常量,xml 里面 id 的名字,如果有 Listener 则会有事件的信息方法等等

BindViews

绑定一群View, FieldCollectionViewBinding 类是一群View的信息,里面有个List<Id>,还有个type也就是一群View的类型

bindResource

绑定资源,比如Anim、String、Drawable
肯定因为绑定,其实也就是赋值的代码不好重复,和 java库工程里没有View内容所以分开的
ResourceBinding 是个接口
FieldAnimationBindingFieldDrawableBindingFieldResourceBindingFieldTypefaceBinding 这4个类继承了 ResourceBinding 接口,就是让这几个类确定 最后生成类文件里的变量 怎样 编写赋值代码

interface ResourceBinding {
  Id id();

  boolean requiresResources(int sdk);

  CodeBlock render(int sdk);
}
final class FieldDrawableBinding implements ResourceBinding {
//移除部分代码
@Override public CodeBlock render(int sdk) {
    if (tintAttributeId.value != NO_RES_ID) {
      return CodeBlock.of("target.$L = $T.getTintedDrawable(context, $L, $L)", name, UTILS, id.code,
          tintAttributeId.code);
    }
    if (sdk >= 21) {
      return CodeBlock.of("target.$L = context.getDrawable($L)", name, id.code);
    }
    return CodeBlock.of("target.$L = $T.getDrawable(context, $L)", name, CONTEXT_COMPAT, id.code);
  }
}

render(int sdk) 根据sdk版本,具体生成出当前版本 Drawable赋值方式
到这里基本知道了ButterKnife是如何帮我们完成findViewById的

学艺不精,如果内容有错误请及时联系我,我及时改正

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,324评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,356评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,328评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,147评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,160评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,115评论 1 296
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,025评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,867评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,307评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,528评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,688评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,409评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,001评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,657评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,811评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,685评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,573评论 2 353