ButterKnife源碼分析

目的:分析ButterKnife如何進(jìn)行view與onClick事件的綁定

原理分析

通過觀察BindView注解發(fā)現(xiàn)阻塑,該注解是存在于編譯器的:

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

那么猜想肯定需要通過注解處理器來處理該注解注釋的字段或方法。
找到引用:

    kapt "com.jakewharton:butterknife-compiler:8.8.1"

找到注解編譯器源碼:ButterKnifeProcessor extends AbstractProcessor{}
通過重寫getSupportedAnnotationTypes方法來獲取支持的注解類型:

  @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;
  }

在獲取到所有的上述類型后,會(huì)進(jìn)入process方法進(jìn)行處理:

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

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();

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

該方法異常簡短贩毕,主要是做了很多的封裝刺彩,其中BindingSet類就是一個(gè)封裝了將要生成的中間類的代碼的類。在該類中躯砰,會(huì)通過解析的注解的屬性來將對應(yīng)的語句添加到BindingSet.Builder中每币,也可通過其中的addMethod向中間類中添加方法。
因此findAndParseTargets方法的主要工作就是解析各個(gè)注解信息并生成對應(yīng)的語句放入BindingSet中琢歇。在所有的解析完畢后兰怠,會(huì)輪詢該Map集合,然后通過JavaFile來生成中間類李茫。
下邊詳細(xì)看一下findAndParseTargets方法:

  private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

    // 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 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;
  }

在該方法中揭保,會(huì)處理收集每種注解類型的信息,上邊只粘出了BindView和OnClick的片段魄宏。以BindView為例:

  private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
      Set<TypeElement> erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // Start by verifying common generated code restrictions.
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
        || isBindingInWrongPackage(BindView.class, element);

    // Verify that the target type extends from 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();

    // Assemble information on the field.
    int id = element.getAnnotation(BindView.class).value();
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    Id resourceId = elementToId(element, BindView.class, id);
    if (builder != null) {
      String existingBindingName = builder.findExistingBindingName(resourceId);
    } else {
      builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }

    String name = simpleName.toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);

    builder.addField(resourceId, new FieldViewBinding(name, type, required));

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

會(huì)收集修飾字段的修飾符秸侣,字段名稱,字段類型等信息娜庇,然后通過builder.addField將該字段添加到BindingSet.Builder中塔次。
在收集到所有的類型后,最終會(huì)通過:

 JavaFile javaFile = binding.brewJava(sdk, debuggable);
 javaFile.writeTo(filer);

生成中間類名秀。binding.brewJava方法:

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

就是直接調(diào)用JavaFile來創(chuàng)建類励负。而createType就是通過在解析時(shí)添加到Binding.Builder中的語句字段來生成類的代碼,比如包名類名等匕得。
最終生成的中間類代碼如下:

  // Generated code from Butter Knife. Do not modify!
package com.jf.jlfund.view.activity;
import butterknife.internal.DebouncingOnClickListener;
import butterknife.internal.Utils;
import com.jf.jlfund.R;

public class FundSaleOutActivity_ViewBinding implements Unbinder {
  private FundSaleOutActivity target;

  private View view2131231970;

  @UiThread
  public FundSaleOutActivity_ViewBinding(FundSaleOutActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public FundSaleOutActivity_ViewBinding(final FundSaleOutActivity target, View source) {
    this.target = target;

    View view;
    target.rootView = Utils.findRequiredViewAsType(source, R.id.rl_fundSaleOut_rootView, "field 'rootView'", RelativeLayout.class);
    target.commonTitleBar = Utils.findRequiredViewAsType(source, R.id.commonTitleBar_fundSaleOut, "field 'commonTitleBar'", CommonTitleBar.class);
    target.etAmount = Utils.findRequiredViewAsType(source, R.id.et_fundSaleOut, "field 'etAmount'", EditText.class);
    view = Utils.findRequiredView(source, R.id.tv_fundSaleOut_saleAll, "field 'tvSaleAll' and method 'onClick'");
    target.tvSaleAll = Utils.castView(view, R.id.tv_fundSaleOut_saleAll, "field 'tvSaleAll'", TextView.class);
    view2131231970 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.onClick(p0);
      }
    });
  }

  @Override
  @CallSuper
  public void unbind() {
    FundSaleOutActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;

    target.rootView = null;
    target.commonTitleBar = null;
    target.etAmount = null;
    target.tvSaleAll = null;

    view2131231970.setOnClickListener(null);
    view2131231970 = null;
  }
}

其中继榆,Utils.findRequiredViewAsType的作用就是source.findViewById并將查找的view轉(zhuǎn)換成具體的類型。

以上的工作都發(fā)生在編譯器汁掠,那么如何在運(yùn)行期來進(jìn)行view的綁定呢略吨?
回想每次在Activity中使用都要事先進(jìn)行:

Unbinder unbinder = ButterKnife.bind(this);

bind方法:

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

看語句是先獲取該Activity的DecorView,然后作為rootView進(jìn)行控件的綁定:


 private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    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 (Exception e) {
        //...
    } 
  }

    @Nullable @CheckResult @UiThread
  private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null) {
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      return null;
    }
    try {
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      //noinspection unchecked
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
    } catch (ClassNotFoundException e) {}
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }

方法說明:

  1. 根據(jù)傳入的target來獲取對應(yīng)的在編譯器生成的target_ViewBinding類考阱。
  2. 獲取該類的構(gòu)造函數(shù)
  3. 通過傳入的參數(shù)執(zhí)行該類的構(gòu)造函數(shù)

上邊方法執(zhí)行完畢后翠忠,在編譯期生成的類就會(huì)被執(zhí)行在Activity的onCreate方法中。在回顧一下生成的類的代碼片段:

    target.rootView = Utils.findRequiredViewAsType(source, R.id.rl_fundSaleOut_rootView, "field 'rootView'", RelativeLayout.class);
    target.commonTitleBar = Utils.findRequiredViewAsType(source, R.id.commonTitleBar_fundSaleOut, "field 'commonTitleBar'", CommonTitleBar.class);

其中乞榨,findRequiredViewAsType就是調(diào)用source.findViewById然后轉(zhuǎn)化成指定的類型秽之。而這個(gè)source就是我們傳入的DecorView当娱。
至此,view的綁定也就完成了考榨。事件的綁定也類似跨细。還有要記得,在Activity的onDestory中要調(diào)用:

 unbinder.unbind();

該操作會(huì)把注解生成的view都給置空河质。

總結(jié)

  1. 通過注解處理器生成對應(yīng)的中間類**_ViewBinding冀惭,并將findViewById與事件點(diǎn)擊等操作寫入該類的構(gòu)造函數(shù)中。
  2. 在運(yùn)行期掀鹅,通過在Activity的onCreate方法中bind(this)來獲取該中間構(gòu)造函數(shù)散休,并通過反射構(gòu)造函數(shù)來實(shí)現(xiàn)view的綁定。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末乐尊,一起剝皮案震驚了整個(gè)濱河市溃槐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌科吭,老刑警劉巖昏滴,帶你破解...
    沈念sama閱讀 217,084評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異对人,居然都是意外死亡谣殊,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,623評論 3 392
  • 文/潘曉璐 我一進(jìn)店門牺弄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來姻几,“玉大人,你說我怎么就攤上這事势告∩甙疲” “怎么了?”我有些...
    開封第一講書人閱讀 163,450評論 0 353
  • 文/不壞的土叔 我叫張陵咱台,是天一觀的道長络拌。 經(jīng)常有香客問我,道長回溺,這世上最難降的妖魔是什么春贸? 我笑而不...
    開封第一講書人閱讀 58,322評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮遗遵,結(jié)果婚禮上萍恕,老公的妹妹穿的比我還像新娘。我一直安慰自己车要,他們只是感情好允粤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,370評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般类垫。 火紅的嫁衣襯著肌膚如雪绳姨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,274評論 1 300
  • 那天阔挠,我揣著相機(jī)與錄音,去河邊找鬼脑蠕。 笑死购撼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的谴仙。 我是一名探鬼主播迂求,決...
    沈念sama閱讀 40,126評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼晃跺!你這毒婦竟也來了揩局?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,980評論 0 275
  • 序言:老撾萬榮一對情侶失蹤掀虎,失蹤者是張志新(化名)和其女友劉穎凌盯,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體烹玉,經(jīng)...
    沈念sama閱讀 45,414評論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡驰怎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,599評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了二打。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片县忌。...
    茶點(diǎn)故事閱讀 39,773評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖继效,靈堂內(nèi)的尸體忽然破棺而出症杏,到底是詐尸還是另有隱情,我是刑警寧澤瑞信,帶...
    沈念sama閱讀 35,470評論 5 344
  • 正文 年R本政府宣布厉颤,位于F島的核電站,受9級特大地震影響凡简,放射性物質(zhì)發(fā)生泄漏走芋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,080評論 3 327
  • 文/蒙蒙 一潘鲫、第九天 我趴在偏房一處隱蔽的房頂上張望翁逞。 院中可真熱鬧,春花似錦溉仑、人聲如沸挖函。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,713評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怨喘。三九已至津畸,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間必怜,已是汗流浹背肉拓。 一陣腳步聲響...
    開封第一講書人閱讀 32,852評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留梳庆,地道東北人暖途。 一個(gè)月前我還...
    沈念sama閱讀 47,865評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像膏执,于是被迫代替她去往敵國和親驻售。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,689評論 2 354

推薦閱讀更多精彩內(nèi)容

  • butterknife注解框架相信很多同學(xué)都在用更米,但是你真的了解它的實(shí)現(xiàn)原理嗎欺栗?那么今天讓我們來看看它到底是怎么實(shí)...
    打不死的小強(qiáng)qz閱讀 645評論 0 4
  • 博文出處:ButterKnife源碼分析,歡迎大家關(guān)注我的博客征峦,謝謝迟几! 0x01 前言 在程序開發(fā)的過程中,總會(huì)有...
    俞其榮閱讀 2,027評論 1 18
  • 原理: APT(Annotation Processing Tool)編譯時(shí)解析技術(shù)(現(xiàn)在已經(jīng)改成了谷歌的更強(qiáng)大的...
    gogoingmonkey閱讀 236評論 0 2
  • 主目錄見:Android高級進(jìn)階知識(這是總目錄索引)?前面我們已經(jīng)講完[編譯期注解的使用例子]大家應(yīng)該對這個(gè)流程...
    ZJ_Rocky閱讀 1,484評論 0 8
  • fromkeys 接受 1 一個(gè)可以迭代的對象 2 值 value 通過迭代對象的key找到對應(yīng)值栏笆,然后把接受到的...
    正在努力ing閱讀 243評論 0 0