學習Butterknife的一點心得(系列)二

Android Butterknife 是一款不錯的註解框架,和咱們在運行期處理註解的思想不一樣,它是在編譯期的時候收集註解信息。在系列一中咱們講解了編譯註解的原理,ButterKnife的原理和那個例子差很少。java

理解這個ButterKnife先從應用講起。先在項目中添加butterKnife.jar,這個能夠從github上找到。git

通常在activity聲明一個View:github

第一步:就能夠這麼寫框架

@Bind(R.id.iv_title_left)maven

ImageView iv_title_left;ide

這樣省去了findViewById方法。ui

第二步:在onCreate()方法中,添加這行代碼:ButterKnife.bind(this);記得要在setContentView(R.layout.activity_main);以後寫。。通過以上兩步配置好了。那麼有兩個問題,在ButterKnife.bind()幹了什麼,框架是怎麼處理這個註解的。this

那咱們帶着問題來看他的源碼,spa


一 編譯期rest

咱們先找框架的註解(咱們用了Bind類型的註解,就找它的源碼):

@Retention(CLASS) @Target(FIELD)
public @interface Bind {
  /** View ID to which the field will be bound. */
  int[] value();
}

這個定義就符合咱們在用的Bind註解。

而後咱們找它的註解處理器:

接着看它process()方法:

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
1  Map<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);

2  for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
    TypeElement typeElement = entry.getKey();
    BindingClass bindingClass = entry.getValue();

    try {
3      bindingClass.brewJava().writeTo(filer);
    } catch (IOException e) {
      error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
          e.getMessage());
    }
  }

  return true;

targetClassMap的key爲一個類元素,value爲類中的註解元素信息。進入1中的findAndParseTarget()方法,由於方法中代碼比較多,只貼一部分,:

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)) {
    try {
      parseBind(element, targetClassMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, Bind.class, e);
    }
  }

  // Process each annotation that corresponds to a listener.
  for (Class<? extends Annotation> listener : LISTENERS) {
    findAndParseListener(env, listener, targetClassMap, erasedTargetNames);
  }

  // Process each @BindArray element.
  for (Element element : env.getElementsAnnotatedWith(BindArray.class)) {
    try {
      parseResourceArray(element, targetClassMap, erasedTargetNames);
    } catch (Exception e) {
      logParsingError(element, BindArray.class, e);
    }
  }

根據幾個for循環,咱們看到了

env.getElementsAnnotatedWith(Bind.class)

的相似寫法,說明是在收集項目中被註解元素的信息。

而後咱們進入paseBind()看具體是怎麼解析的:

private void parseBind(Element element, Map<TypeElement, BindingClass> targetClassMap,
    Set<String> erasedTargetNames) {
  // Verify common generated code restrictions.
  if (isInaccessibleViaGeneratedCode(Bind.class, "fields", element)
      || isBindingInWrongPackage(Bind.class, element)) {
    return;
  }

  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)) {
    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);
  }
}

咱們在應用Bind註解的時候,一般只用了一個ID,因此咱們進入paserBindOne():

方法代碼比較多,就貼核心代碼:

int id = ids[0];
BindingClass bindingClass = targetClassMap.get(enclosingElement);
if (bindingClass != null) {
  ViewBindings viewBindings = bindingClass.getViewBinding(id);
  if (viewBindings != null) {
    Iterator<FieldViewBinding> iterator = viewBindings.getFieldBindings().iterator();
    if (iterator.hasNext()) {
      FieldViewBinding existingBinding = iterator.next();
      error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
          Bind.class.getSimpleName(), id, existingBinding.getName(),
          enclosingElement.getQualifiedName(), element.getSimpleName());
      return;
    }
  }
} else {
  bindingClass = getOrCreateTargetClass(targetClassMap, enclosingElement);
}

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

FieldViewBinding binding = new FieldViewBinding(name, type, required);
bindingClass.addField(id, binding);

// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement.toString());


能夠看到若是若是類對應的BindClass不存在的話,就建立一個。

FieldViewBinding binding = new FieldViewBinding(name, type, required);
bindingClass.addField(id, binding);

能夠看到根據註解元素的名字,類型(獲取這些值調用的方法的含義,我在例子中解釋了),建立了一個註解字段對應的FieldViewBinding,而後添加到bindingClass中。

如今切回到findPaseTargets()方法,其餘對不一樣註解類型的解析基本和這個上述對註解Bind解析的流程同樣。執行完以後就收集到了全部的註解信息,暫存到targetClassMap中。


接着退回到process()方法,3行代碼的意思爲每個有註解的類調用brewJava()方法,接着調用writeTo(),查看源代碼writeTo()發現是其javapoet的代碼,而javapoet是一個生成java源文件的框架。(javapoet是butterKnife經過maven管理引用的一個jar包)。而後進入brewJava()方法看一下:

JavaFile brewJava() {
1  TypeSpec.Builder result = TypeSpec.classBuilder(className)
2      .addModifiers(PUBLIC)
3      .addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));

4  if (parentViewBinder != null) {
5    result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentViewBinder),
6        TypeVariableName.get("T")));
7  } else {
8    result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T")));
  }

9  result.addMethod(createBindMethod());
10  result.addMethod(createUnbindMethod());

  return JavaFile.builder(classPackage, result.build())
      .addFileComment("Generated code from Butter Knife. Do not modify!")
      .build();
}

看生成了JavaFile對象,先不分析它的源碼,看看它生成的文件,文件路徑爲:

Weather\.apt_generated\com\hsj\weather\ui\activity\MainActivity$$ViewBinder.java,Weather是個人項目名,

文件內容是:

public class MainActivity$$ViewBinder<T extends com.hsj.weather.ui.activity.MainActivity> implements ViewBinder<T> {
  @Override public void bind(final Finder finder, final T target, Object source) {
    View view;
    view = finder.findRequiredView(source, 2131361899, "field 'iv_title_left'");
    target.iv_title_left = finder.castView(view, 2131361899, "field 'iv_title_left'");
    view = finder.findRequiredView(source, 2131361901, "field 'iv_title_right'");
    target.iv_title_right = finder.castView(view, 2131361901, "field 'iv_title_right'");
    view = finder.findRequiredView(source, 2131361900, "field 'tv_title_center'");
    target.tv_title_center = finder.castView(view, 2131361900, "field 'tv_title_center'");
  }

  @Override public void unbind(T target) {
    target.iv_title_left = null;
    target.iv_title_right = null;
    target.tv_title_center = null;
  }
}

再回頭看brewJava()方法,1-8行生成了這個源文件的類頭,9行對應生成了bind()方法(根createBindMethod()這個方法名字能夠猜出來)10行對應生成unBind()方法。具體怎麼生成的能夠查看源代碼,挺簡單的,這裏不分析了。至此框架在編譯期的處理結束了。

相關文章
相關標籤/搜索