1. 程式人生 > >ButterKnife(8.4.0版本)原理分析

ButterKnife(8.4.0版本)原理分析

        ButterKnife是鼎鼎大名的JakeWharton寫的註解框架, 將你從findViewById這樣無聊的體力活解脫出來。  github地址: https://github.com/JakeWharton/butterknife  , 已超過1萬顆星了,   很屌。

       JakeWharton是square公司的大咖,  是Piccaso(圖片開源框架)、RxJava/RxAndroid(響應式程式設計)、OkHttp(Http通訊開源框架)的主要開發者。 不誇張的說, 對於一個做網際網路app的碼農來說, 如果不知道他就是少見識了。

       ButterKnife的整合方式和使用方法已在github上描述,    就不多說了。  8.4.0版本做了一些優化:

1、 刪除了ButterKnife類的unbind方法。  改為儲存ButterKnife.bind函式返回的Unbinder引用, 在onDestroy函式裡呼叫Unbinder的unbind方法。 

2、如下圖所示,在編譯過程會生成*_ViewBinding.java,  如SimpleActivity_ViewBinding.java。

3、ButterKnife對效能沒影響,  一些人說用了註解和反射影響效能, 這個鍋ButterKnife不背。 增加Java方法數量和apk體積到是真的, 畢竟生成了_ViewBinding.java檔案。

基礎知識:APT(Annotation Processing Tool)是一種處理註釋的工具,它對原始碼檔案進行檢測找出其中的Annotation,使用Annotation進行額外的處理。 Annotation處理器在處理Annotation時可以根據原始檔中的Annotation生成額外的原始檔和其它的檔案(檔案具體內容由Annotation處理器的編寫者決定), APT還會編譯生成的原始檔和原來的原始檔,將它們一起生成class檔案。

生成*_ViewBinding.java檔案的原理:在編譯時期,javac會呼叫java註解處理器(APT)進行處理,通過自定義註解處理器來實現想要的功能, 例如ButterKnife在編譯期間生成Java檔案。

下面看ButterKnife的核心程式碼ButterKnifeProcessor.java, 在Android編譯期間

@AutoService(Processor.class)   //註冊處理器,這樣在編譯時才會呼叫ButterKnifeProcessor
public final class ButterKnifeProcessor extends AbstractProcessor {
  ...
  //支援的監聽器
 private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(//
      OnCheckedChanged.class, //
      OnClick.class, //
      OnEditorAction.class, //
      OnFocusChange.class, //
      OnItemClick.class, //
      OnItemLongClick.class, //
      OnItemSelected.class, //
      OnLongClick.class, //
      OnPageChange.class, //
      OnTextChanged.class, //
      OnTouch.class //
  );
  //支援註解的資源型別
  private static final List<String> SUPPORTED_TYPES = Arrays.asList(
      "array", "attr", "bool", "color", "dimen", "drawable", "id", "integer", "string"
  );
  ...   

  //指定使用的Java版本
  @Override public synchronized void init(ProcessingEnvironment env) {
    super.init(env);
    ...
  }

  //返回stirng型別的set集合,集合裡包含了需要處理的註解</span></code>型別
  @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(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(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);

    return annotations;
  }

  //核心函式, 在這個函式裡生成*_ViewBinding.java
  @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //查詢所有的註解資訊,並形成BindingClass儲存到 map中
   Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env); 

    //遍歷bindingMap生成類名_ViewBinding的java檔案
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();

      JavaFile javaFile = binding.brewJava(sdk);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return true;
  }

  //解析所有的註解
  private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

    scanForRClasses(env);

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

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

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

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

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

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

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

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

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

    // Process each @BindView element.
    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);
      }
    }

    // Process each @BindViews element.
    for (Element element : env.getElementsAnnotatedWith(BindViews.class)) {
      // we don't SuperficialValidation.validateElement(element)
      // so that an unresolved View type can be generated by later processing rounds
      try {
        parseBindViews(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindViews.class, e);
      }
    }

    // Process each annotation that corresponds to a listener.
    for (Class<? extends Annotation> listener : LISTENERS) {
      findAndParseListener(env, listener, 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();

      TypeElement parentType = findParentType(type, erasedTargetNames);
      if (parentType == null) {
        bindingMap.put(type, builder.build());
      } else {
        BindingSet parentBinding = bindingMap.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;
  }
  ...
}

   下面看Java檔案的生成方法:
  JavaFile brewJava() {
  //引數1:包名
   //引數2:TypeSpec,這個可以生成class ,interface 等java檔案
    //注意addFileComment的引數,已經說明是生成的程式碼了
   return JavaFile.builder(bindingClassName.packageName(), createBindingClass())
        .addFileComment("<span style="color:#FF0000;">Generated code from Butter Knife. Do not modify!</span>")
        .build();  }

bind:

如何才能生成的Java檔案呢?

答案是: ButterKnife.bind(this);方法。

unbind:

在新版的8.4.0中去除了 unbind方法。

<span style="font-size:18px;">ButterKnife.unbind  //已經刪除了</span>

並採用了介面的形式,讓生成的類來實現釋放引用。 例如:

<span style="font-size:18px;"> public final class SimpleAdapter$ViewHolder_ViewBinding implements Unbinder {
   @UiThread
  public SimpleAdapter$ViewHolder_ViewBinding(SimpleAdapter.ViewHolder target, View ) {
  //...
  }
 //...
  @Override
  public void unbind() {
  //...
  }
 }</span>

那如何unbind呢?ButterKnife.bind(this)返回值是一個Unbinder引用。
  所以可以這樣:

<span style="font-size:18px;"> Unbinder mUnbinder;     
 @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
     mUnbinder=ButterKnife.bind(this);  //儲存引用
    }
      @Override
  protected void onDestroy() {
     super.onDestroy();
    mUnbinder.unbind();  //釋放所有繫結的view
  }</span>

     綜上, ButterKnife是個好東西,節省開發時間而且不影響效能。 是Android開發居家必備的良品。