ButterKnifeソースコード解析(1)

15444 ワード

ButterKnifeは注釈によって動的に注入されていますが、注釈についてまだよく知らない場合は、前の注釈に関する文章を見て注釈を見ることができます.
ButterKnifeの動作原理は一般的に2つのステップに分けられます.
  • @Bind(R.id.),@OnClick(R.id.)などの注記によりコンパイル時にjavaファイルが動的に生成され、javaファイルコンパイラが生成することで対応するclassファイル
  • にコンパイルされる.
  • はButterKnifeを通過する.bind(this)など同様の方法でIDを対応するコンテキストにバインドする.

  • まず、最も基本的な注釈クラスを見てみましょう.
    @Retention(CLASS) @Target(FIELD)
    public @interface Bind {
     /** View ID to which the field will be bound. */
    int[] value();
    }
    

    これが最もよく使われるBind注釈で、バインドするviewを識別するのに一般的に使われています.ここでは、このviewがコンパイル時に機能し、メンバー変数を識別するためにしか使用できないことが明らかになります.これは、コンパイル時に解析してjavaソースコードを生成できることを示しています.
    ButterKnifeの注釈解析器はButterknifeProcessorであり、その最も主要な方法は以下の通りである.
    // 
      @Override public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
    
        elementUtils = env.getElementUtils();
        typeUtils = env.getTypeUtils();
        filer = env.getFiler();
      }
    
    // 
      @Override public Set getSupportedAnnotationTypes() {
        Set types = new LinkedHashSet();
    
        types.add(Bind.class.getCanonicalName());
    
        for (Class extends Annotation> listener : LISTENERS) {
          types.add(listener.getCanonicalName());
        }
    
        types.add(BindBool.class.getCanonicalName());
        types.add(BindColor.class.getCanonicalName());
        types.add(BindDimen.class.getCanonicalName());
        types.add(BindDrawable.class.getCanonicalName());
        types.add(BindInt.class.getCanonicalName());
        types.add(BindString.class.getCanonicalName());
    
        return types;
      }
    
    // 
      @Override public boolean process(Set extends TypeElement> elements, RoundEnvironment env) {
        Map targetClassMap = findAndParseTargets(env);
    
        for (Map.Entry entry : targetClassMap.entrySet()) {
          TypeElement typeElement = entry.getKey();
          BindingClass bindingClass = entry.getValue();
    
          try {
            JavaFileObject jfo = filer.createSourceFile(bindingClass.getFqcn(), typeElement);
            Writer writer = jfo.openWriter();
            writer.write(bindingClass.brewJava());
            writer.flush();
            writer.close();
          } catch (IOException e) {
            error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
                e.getMessage());
          }
        }
    
        return true;
      }
    

    ButterKnifeによって注釈された要素はすべてprocessメソッドに移行し、processメソッドではまずfindAndParseTargets(env)メソッドによって注釈されたクラスを持つすべての情報をBindingClassの中に個別に配置し、最後にType Elementをキーとし、BindingClassを値とするキー値ペアを形成する.
    次に、このキー値ペアをループし、TypeElementとBindingClassの情報に基づいて対応するjavaクラスを生成するだけです.たとえば、MainActivityで生成されたクラスは、MainActivity$$ViewBinderクラスです.
    次にfindAndParseTargets(env)メソッドを見てみましょう.
     private Map findAndParseTargets(RoundEnvironment env) {
        Map targetClassMap = new LinkedHashMap();
        Set 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);
        }
    
        //....
    
        // Try to find a parent binder for each.
        for (Map.Entry entry : targetClassMap.entrySet()) {
          String parentClassFqcn = findParentFqcn(entry.getKey(), erasedTargetNames);
          if (parentClassFqcn != null) {
            entry.getValue().setParentViewBinder(parentClassFqcn + SUFFIX);
          }
        }
    
        return targetClassMap;
      }
    

    この方法の主な仕事は各注釈を分割して遍歴して、そして解析を行って、最後に解析の結果をtargetClassMapに入れて、ここでBind注釈の解析を重点的に見ます:
    private void parseBind(Element element, Map targetClassMap,
          Set 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);
        }
      }
    

    ここでButterKnifeは値によって異なる解析方式をとることがわかりますが、ここではparseBindOne(element,targetClassMap,erasedTargetName)という最も一般的な解析方式を見てみましょう.これも解析器の最も核心的なところです
    private void parseBindOne(Element element, Map targetClassMap,
          Set erasedTargetNames) {
        boolean hasError = false;
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    
        // Verify that the target type extends from View.
        TypeMirror elementType = element.asType();
        if (elementType.getKind() == TypeKind.TYPEVAR) {
          TypeVariable typeVariable = (TypeVariable) elementType;
          elementType = typeVariable.getUpperBound();
        }
        if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
          error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
              Bind.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName());
          hasError = true;
        }
    
        // Assemble information on the field.
        int[] ids = element.getAnnotation(Bind.class).value();
        if (ids.length != 1) {
          error(element, "@%s for a view must only specify one ID. Found: %s. (%s.%s)",
              Bind.class.getSimpleName(), Arrays.toString(ids), enclosingElement.getQualifiedName(),
              element.getSimpleName());
          hasError = true;
        }
    
        if (hasError) {
          return;
        }
    
        int id = ids[0];
        BindingClass bindingClass = targetClassMap.get(enclosingElement);
        if (bindingClass != null) {
          ViewBindings viewBindings = bindingClass.getViewBinding(id);
          if (viewBindings != null) {
            Iterator 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();
        String type = elementType.toString();
        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());
      }
    

    この方法ではenclosingElementにBind注釈クラスを用いた情報が含まれており,以下の方法で注釈注釈の値を取得する.
        int[] ids = element.getAnnotation(Bind.class).value();
    

    次のタスクは、BindingClassを作成していない場合(ない場合)、BindingClassクラスを作成するのも簡単で、そのコンストラクション関数もパッケージ名、クラス名などの基本情報しか必要ありません.
      private BindingClass getOrCreateTargetClass(Map targetClassMap,
          TypeElement enclosingElement) {
        BindingClass bindingClass = targetClassMap.get(enclosingElement);
        if (bindingClass == null) {
          String targetType = enclosingElement.getQualifiedName().toString();
          String classPackage = getPackageName(enclosingElement);
          String className = getClassName(enclosingElement, classPackage) + SUFFIX;
    
          bindingClass = new BindingClass(classPackage, className, targetType);
          targetClassMap.put(enclosingElement, bindingClass);
        }
        return bindingClass;
      }
    

    その後、viewの情報をFieldViewBindingクラスのインスタンスにバインドし、最後に対応するBindingClassインスタンスに追加します.これにより、注釈を使用するインスタンスの注釈に関するすべての情報と、そのエンティティクラス自体の情報がこのBindingClassクラスのエンティティクラスに追加され、ButterKnifeはこのクラスを利用して対応するjavaファイルを生成します.
    次にバインドする手順は、上述した手順と同様であり、基本的にはこの原理であり、ここでは説明しないが、解析が完了すると、注釈を使用するすべてのインスタンスに関する情報はBindingClassの集合内に格納される.次に行う作業は、このコレクションをループしてファイルを生成することです.
    生成された文字列はbindingClassである.brewJava()はjavaファイル内のすべての文字列を生成する方法で実現されます.
    String brewJava() {
        StringBuilder builder = new StringBuilder();
        builder.append("// Generated code from Butter Knife. Do not modify!
    "); builder.append("package ").append(classPackage).append(";

    "); if (!resourceBindings.isEmpty()) { builder.append("import android.content.res.Resources;
    "); } if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) { builder.append("import android.view.View;
    "); } builder.append("import butterknife.ButterKnife.Finder;
    "); if (parentViewBinder == null) { builder.append("import butterknife.ButterKnife.ViewBinder;
    "); } builder.append('
    '); builder.append("public class ").append(className); builder.append(""); if (parentViewBinder != null) { builder.append(" extends ").append(parentViewBinder).append(""); } else { builder.append(" implements ViewBinder"); } builder.append(" {
    "); emitBindMethod(builder); builder.append('
    '); emitUnbindMethod(builder); builder.append("}
    "); return builder.toString(); }

    この方法はまずパッケージ名を生成し,次に各クラスを導入し,次にクラスのトピックを生成し,emitBindMethod(builder)メソッドによりクラスのbind(builder)メソッドを生成し,emitUnbindMethod(builder)メソッドによりクラスのunbind(builder)メソッドを生成する.
      private void emitBindMethod(StringBuilder builder) {
      builder.append("  @Override ")
          .append("public void bind(final Finder finder, final T target, Object source) {
    "); // Emit a call to the superclass binder, if any. if (parentViewBinder != null) { builder.append(" super.bind(finder, target, source);

    "); } if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) { // Local variable in which all views will be temporarily stored. builder.append(" View view;
    "); // Loop over each view bindings and emit it. for (ViewBindings bindings : viewIdMap.values()) { emitViewBindings(builder, bindings); } // Loop over each collection binding and emit it. for (Map.Entry entry : collectionBindings.entrySet()) { emitCollectionBinding(builder, entry.getKey(), entry.getValue()); } } if (!resourceBindings.isEmpty()) { builder.append(" Resources res = finder.getContext(source).getResources();
    "); for (FieldResourceBinding binding : resourceBindings) { builder.append(" target.") .append(binding.getName()) .append(" = res.") .append(binding.getMethod()) .append('(') .append(binding.getId()) .append(");
    "); } } builder.append(" }
    "); } private void emitViewBindings(StringBuilder builder, ViewBindings bindings) { builder.append(" view = "); List requiredViewBindings = bindings.getRequiredBindings(); if (requiredViewBindings.isEmpty()) { builder.append("finder.findOptionalView(source, ") .append(bindings.getId()) .append(", null);
    "); } else { if (bindings.getId() == View.NO_ID) { builder.append("target;
    "); } else { builder.append("finder.findRequiredView(source, ") .append(bindings.getId()) .append(", \""); emitHumanDescription(builder, requiredViewBindings); builder.append("\");
    "); } } emitFieldBindings(builder, bindings); emitMethodBindings(builder, bindings); }

    ここで各バインディングviewの構成(ViewBindings)を読み出し、次にviewをバインディングし、activityの対応するviewを最初に見つけ、emitFieldBindings(builder,bindings)によってメンバー変数のviewをactivityに注入し、emitMethodBindings(builder.bindings)はイベントをバインディングするために使用されることがわかります.ここでは次の文章で詳しく解説します.
    これらのすべてが終了すると、ファイルを生成できます.コンパイル段階で生成されたjavaファイルもclassファイルを生成します.ここでは、読者の理解を容易にするために、最も基本的なMainActivityクラスと生成されたMainActivity&&ViewBindlerクラスのソースコードを提供します.
    //MainActivity.java
    package com.fyales.butterknifedemo;
    
    import android.os.Bundle;
    import android.support.v7.app.AppCompatActivity;
    import android.widget.Button;
    import android.widget.TextView;
    import android.widget.Toast;
    
    import butterknife.Bind;
    import butterknife.ButterKnife;
    import butterknife.OnClick;
    
    public class MainActivity extends AppCompatActivity {
    
        @Bind(R.id.text_tv)
        TextView textTv;
        @Bind(R.id.click_btn)
        Button clickBtn;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            ButterKnife.bind(this);
        }
    
        @OnClick(R.id.text_tv)
        void textTvClick(){
            Toast.makeText(this,"hello world",Toast.LENGTH_SHORT).show();
        }
    }
    
    //MainActivity$$ViewBinder
    // Generated code from Butter Knife. Do not modify!
    package com.fyales.butterknifedemo;
    
    import android.view.View;
    import butterknife.ButterKnife.Finder;
    import butterknife.ButterKnife.ViewBinder;
    
    public class MainActivity$$ViewBinder implements ViewBinder {
      @Override public void bind(final Finder finder, final T target, Object source) {
        View view;
        view = finder.findRequiredView(source, 2131492943, "field 'textTv' and method 'textTvClick'");
        target.textTv = finder.castView(view, 2131492943, "field 'textTv'");
        view.setOnClickListener(
          new butterknife.internal.DebouncingOnClickListener() {
            @Override public void doClick(
              android.view.View p0
            ) {
              target.textTvClick();
            }
          });
        view = finder.findRequiredView(source, 2131492944, "field 'clickBtn'");
        target.clickBtn = finder.castView(view, 2131492944, "field 'clickBtn'");
      }
    
      @Override public void unbind(T target) {
        target.textTv = null;
        target.clickBtn = null;
      }
    }
    

    ここまで、ButterKnifeコンパイラ生成ファイル部分のコード分析を完了しました.詳細は、ButterKnifeソースコードを自分で読んでください.何か漏れがあれば、次はButterKnifeとコンテキストバインドを分析します.