ButterKnife原理及源碼淺析

知其然知其所以然

ButterKnife使用Java Annotation Processing技術,在Java代碼編譯成Java字節碼的時候處理注解@BindView、@OnClick、@BindXXX(ButterKnife支持的注解:在Butterknife-annotations包下)生成對應的ViewBinding類,在這個類中進行資源和視圖的綁定操作.


ButterKnife流程圖

現在大致流程我們已經清楚了,那么源碼中到底是如何實現的呢?
我們先看下源碼工程的目錄(ButterKnife版本:8.8.1):


ButterKnife工程目錄
Module對應的職責如下:
  • butterknife: android library model 提供android使用的API
  • butterknife-annotations: java-model,使用時的注解
  • butterknife-compiler: java-model,編譯時用到的注解的處理器
  • butterknife-gradle-plugin: 自定義的gradle插件,輔助生成有關代碼
  • butterknife-integration-test: 項目的測試用例
  • butterknife-lint:項目的lint檢查
首先我們先看下butterknife的構成

看起來真是簡潔啊~ 我們來依次看下吧~

DebouncingOnClickListener

這是一個抽象類實現了View.OnClickListener并做了相應的處理來消除同一幀中發布的多個點擊,當點擊一個按鈕將禁用該框架的所有按鈕。

    public abstract class DebouncingOnClickListener implements View.OnClickListener {
      static boolean enabled = true;

      private static final Runnable ENABLE_AGAIN = new Runnable() {
        @Override public void run() {
          enabled = true;
        }
      };
    
      @Override public final void onClick(View v) {
        if (enabled) {
          enabled = false;
          v.post(ENABLE_AGAIN);
          doClick(v);
        }
      }
    
      public abstract void doClick(View v);
    }

ImmutableList

這是一個被final修飾繼承AbstractList方法的不可變輕量集合類,因為在實際使用時需要的方法過少,所有沒有必要去使用ArrayList,它的實現也很簡單。

final class ImmutableList<T> extends AbstractList<T> implements RandomAccess {
  private final T[] views;

  ImmutableList(T[] views) {
    this.views = views;
  }

  @Override public T get(int index) {
    return views[index];
  }

  @Override public int size() {
    return views.length;
  }

  @Override public boolean contains(Object o) {
    for (T view : views) {
      if (view == o) {
        return true;
      }
    }
    return false;
  }
}

Utils

這個類看名字就知道是個工具類,它的作用主要就是獲取資源和類型強轉,代碼就省略了,參考價值不大...

ButterKnife相關方法解析

  • ButterKnife.bind()方法

    • bind(Activity target):這個方法會先根據Activity取得decorView,然后再調用createBinding(target,decorView)方法。
    • bind(View target): 這個方法比較特殊,它會將視圖及其子視圖用作視圖根,調用createBinding(target,target)方法。
    • bind(Dialog target):這個方法會先根據Dialog取得decorView,然后再調用createBinding(target,decorView)方法。
    • bind(Object target, Activity source):同bind(Activity target)。
    • bind(Object target, Dialog source):同bind(Dialog target)
    • bind(Object target, View source):直接調用createBinding(target,source)方法。

    參數中的target其實就是發生視圖綁定的地方,也就是我們使用@BindXXX注解所在的類。所以在ViewHodler中調用的方法應該是最后一個

  • ButterKnife.createBinding()方法
    在前面的bind方法中最后都調用了這個方法來結束,那這個方法中到底有什么呢?

/**
 *
 * @param target
 * @param source target所在的根view或者自身(if target == view)
 * @ret urn
 */
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
  Class<?> targetClass = target.getClass();
  if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
  Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

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

  //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
  try {
    return constructor.newInstance(target, source);
  } catch (IllegalAccessException e) {
    throw new RuntimeException("Unable to invoke " + constructor, e);
  } catch (InstantiationException e) {
    throw new RuntimeException("Unable to invoke " + constructor, e);
  } catch (InvocationTargetException e) {
    Throwable cause = e.getCause();
    if (cause instanceof RuntimeException) {
      throw (RuntimeException) cause;
    }
    if (cause instanceof Error) {
      throw (Error) cause;
    }
    throw new RuntimeException("Unable to create binding instance.", cause);
  }
}

@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
  //從緩存里尋找是否已經綁定過
  Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
  if (bindingCtor != null) {
    if (debug) Log.d(TAG, "HIT: Cached in binding map.");
    return bindingCtor;
  }

  //類名檢測
  String clsName = cls.getName();
  if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
    if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
    return null;
  }

  try {
    //加載對應的Class_ViewBinding類文件
    Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
    //noinspection unchecked
    //沒有檢查
    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;
}

首先獲取到target的class,然后根據class去緩存中尋找是否已經綁定過,綁定過就直接返回,否則取得類名并檢查類名是否是android.或者java.開頭的,如果是的話就直接返回null(ps:這里是禁止去接觸framework層的代碼),檢查通過之后就會去尋找在編譯以后生成的對應XX_ViewBinding類文件通過反射調用構造方法來進行綁定。如果沒有找到對應的XX_ViewBinding類文件會在target的父類中繼續尋找直到找到或者失敗拋異常。最終如果綁定成功以后會加入緩存池中并返回。

接著我們先看下butterknife-annotations的構成
butterknife支持的全部注解

不得不說這些注解已經能夠基本滿足我們日常工作了。那我就挑幾個大家不常用或者不熟悉的注解來說下怎么使用吧~

@OnCheckedChanged

這個注解的作用主要是在android.widget.CompoundButton上,但是當你去看源碼時發現這只是個抽象類,那么它的實現類到底是誰?在官網上我們看到分別是:CheckBox,RadioButton,Switch,ToggleButton



這個注解對應的替代的方法就是setOnCheckedChangeListener,使用如下:

 @OnCheckedChanged(R.id.example) void onChecked(boolean checked) {
    Toast.makeText(this, checked ? "Checked!" : " Unchecked!",Toast.LENGTH_SHORT).show();  
  }
@OnTextChanged

這個注解是用來替代addTextChangedListener。以前我們在對EditText進行文本變化監聽時不得不重寫TextChangedListener的三個方法,但是很多時候我們只需要用到其中一個方法。這個時候使用這個注解就能滿足我們的要求,注解內部使用枚舉變量分別三個方法,我們在使用注解時傳入對應的枚舉變量對應的方法就會被回調,使用方法如下:

//因為在注解定義時默認返回TEXT_CHANGED 所以callback = TEXT_CHANGED時可以省略
@OnTextChanged(R.id.example) void onTextChanged(CharSequence text) {
   Toast.makeText(this, "Text changed: " + text, Toast.LENGTH_SHORT).show();
}
@OnTextChanged(value = R.id.example, callback = BEFORE_TEXT_CHANGED)
  void onBeforeTextChanged(CharSequence text) {
    Toast.makeText(this, "Before text changed: " + text,Toast.LENGTH_SHORT).show();
}

@OnTextChanged(value = R.id.example, callback = AFTER_TEXT_CHANGED)
  void onAfterTextChanged(CharSequence text) {
    Toast.makeText(this, "Before text changed: " + text,Toast.LENGTH_SHORT).show();
}

這里就不對所有注解怎么使用進行贅述了,大家只要在使用時去翻一翻源碼即可.

然后我們先看下butterknife-compiler的構成

butterknife在編譯過程中進行的操作全部在這里

因為處理注解使用的是Java Annotation Processing,所以入口肯定是ButterKnifeProcessor。
在看這個類的源碼之前先說一點關于JavaPoet的知識

  • MethodSpec 代表一個構造函數或方法聲明。
  • TypeSpec 代表一個類,接口,或者枚舉聲明。
  • FieldSpec 代表一個成員變量,一個字段聲明。
  • JavaFile包含一個頂級類的Java文件。
舉個小栗子??

這是一個HelloLianJia類

package com.lianjia.hello;

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

使用JavaPoet來生成上面的HelloLianJia類

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, World!")
    .build();

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

JavaFile javaFile = JavaFile.builder("com.lianjia.hello", helloWorld)
    .build();

javaFile.writeTo(System.out);

上述代碼中我們先創建了一個MethodSpec來聲明main方法,它配置了修飾符,返回類型,參數和代碼語句。 然后我們使用TypeSpec創建了HelloLianJia類,它配置了修飾符之后將main方法添加到HelloLianJia類中,最后通過JavaFile將其添加到HelloWorld.java文件中。

在了解了JavaPoet相關知識后我們來看ButterKnifeProcessor的process方法
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //1.遍歷和解析注解 結果存在Map中
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    //遍歷Map 通過JavaPoet生成對應的ViewBinding文件
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();
      //2.這里就是ViewBinding生成的過程
      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;
  }

解析的過程就是先將所有注解遍歷解析一遍,然后使用JavaPoet根據解析結果生成對應的XXX_ViewBinding文件.

1. findAndParseTargets
 /**
   * 遍歷和解析注解
   * @param env
   * @return 返回遍歷的結果
   */
  private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    //注意這里創建的Map的value值為BindingSet.Builder 而不是方法定義的返回類型BindingSet
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

    //將相關的View與R.id.XX建立對應的關系
    scanForRClasses(env);

    // Process each @BindAnim element.
    for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {
      if (!SuperficialValidation.validateElement(element)) continue;
      try {
        //1-2.我們在這里只說解析@BindAnim的過程,其他的注解類似
        parseResourceAnimation(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindAnim.class, e);
      }
    }

    .....................省略其他注解的解析過程...........................
    

    // 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).

    // 這里的主要目的就是為了將Map.Entry<TypeElement, BindingSet.Builder>轉化成Map<TypeElement, BindingSet>

    //第一步將Map.Entry<TypeElement, BindingSet.Builder>轉換成Map.Entry<TypeElement, BindingSet.Builder>
    //這么做的主要原因是Map.Entry<K,V>可以直接調用getKey()和getValue()
    //那為什么要使用Deque呢? 看注釋提醒需要將父類的綁定者和它們子類的綁定者關聯起來,然后基于Deque從根到葉子的樹遍歷
    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;
  }
1-2. parseResourceAnimation
  private void parseResourceAnimation(Element element,
    Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) {
    boolean hasError = false;
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Verify that the target type is Animation.檢驗類型是否是Animation類型
    if (!ANIMATION_TYPE.equals(element.asType().toString())) {
      error(element, "@%s field type must be 'Animation'. (%s.%s)", BindAnim.class.getSimpleName(),
        enclosingElement.getQualifiedName(), element.getSimpleName());
      hasError = true;
    }

    // Verify common generated code restrictions.檢驗通用生成的代碼限制。
    hasError |= isInaccessibleViaGeneratedCode(BindAnim.class, "fields", element);
    //檢驗類的包名不能是android.或者java.開頭的
    hasError |= isBindingInWrongPackage(BindAnim.class, element);

    if (hasError) {
      return;
    }

    // Assemble information on the field.
    String name = element.getSimpleName().toString();
    //獲取id
    int id = element.getAnnotation(BindAnim.class).value();
    //將id和對應的packageName存儲起來
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    //根據enclosingElement生成對應的BindingSet.Buidler
    BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    //然后對id和name進行封裝以后添加到builder中
    builder.addResource(new FieldAnimationBinding(getId(qualifiedId), name));
    erasedTargetNames.add(enclosingElement);
  }
1-3. 關于BindingSet.Builder

BindingSet采用的是建造者設計模式來構建對應的實例,在內部類Buidler中會對注解的TypeName、bindingClassName、parentBinding和注解所在的Target是什么進行存儲。

 static final class Builder {
    private final TypeName targetTypeName;
    private final ClassName bindingClassName;
    private final boolean isFinal;
    private final boolean isView;
    private final boolean isActivity;
    private final boolean isDialog;

    private BindingSet parentBinding;
   .........
 }
2. BindingSet.brewJava

這個方法里面處理了所有的注解,包含了ViewBinding類的生成過程

JavaFile brewJava(int sdk, boolean debuggable) {
    return JavaFile.builder(bindingClassName.packageName(), createType(sdk, debuggable))
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  }

  private TypeSpec createType(int sdk, boolean debuggable) {
    //類名,修飾符類型為public
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC);
    
    //final類型判斷
    if (isFinal) {
      result.addModifiers(FINAL);
    }
    //父類的注解綁定判斷
    if (parentBinding != null) {
      //繼承父類
      result.superclass(parentBinding.bindingClassName);
    } else {
      //實現Unbinder接口
      result.addSuperinterface(UNBINDER);
    }

    //target字段的添加
    if (hasTargetField()) {
      result.addField(targetTypeName, "target", PRIVATE);
    }

    //創建構造方法 在最前面的bind方法中view、activity、dialog對應的綁定方法不一樣,同理生成的構造方法也不一樣
    //這里的構造方法其實沒有做什么事情,只是相當于一個重載方法入口的匹配過程
    if (isView) {
      result.addMethod(createBindingConstructorForView());
    } else if (isActivity) {
      result.addMethod(createBindingConstructorForActivity());
    } else if (isDialog) {
      result.addMethod(createBindingConstructorForDialog());
    }

    //綁定是否需要View
    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) {
      //unbind方法的添加過程
      result.addMethod(createBindingUnbindMethod(result));
    }

    return result.build();
  }

至此,ButterKnife的最核心的部分已經講述完畢...

這個時候,可能會有人說:" 那R2是在什么地方生成的呢? "??

R2的出現起初是為了解決ButterKnife在Library中使用時無法使用R進行資源綁定,后來為了統一無論是在什么工程使用統一使用R2,而R2的配置其實是在 butterknife-gradle-plugin里通過gradle插件來實現的.


ButterKnifePlugin

這里就是R2的誕生之地~

butterknife-lint

作為項目檢查,其內部存在一個核心類InvalidR2UsageDetector用來確保生成的R2在注釋外部不被引用,感興趣的朋友可以去看看源碼時怎么實現。逃:)

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 俗話說的好“不想偷懶的程序員,不是好程序員”,我們在日常開發android的過程中,在前端activity或者fr...
    蛋西閱讀 4,982評論 0 14
  • 轉載于:[http://blog.csdn.net/chenkai19920410/article/details...
    雙魚大貓閱讀 534評論 0 5
  • 0X0 前言 做過Android開發的猿類很多都知道ButterKnife這么個東西。這個庫可以大大的簡化我們的代...
    knightingal閱讀 767評論 1 10
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,967評論 19 139
  • 不要總用自己的價值觀去決定別人的道路,每個人都有自己人生。
    楊寧哥哥閱讀 142評論 0 0