Butterknife 源碼剖析(二)

源碼剖析——編譯期解析注解、生成java代碼流程

在上一篇 ButterKnife的工作流程 中我們分析了ButterKnife.bind()的流程乔煞,那么編譯期是如何生成MainActivity$$ViewBinder.java呢?答案將在本篇揭曉烁涌。

編譯時 Annotation 指 @Retention 為 CLASS 的 Annotation袖牙,甴 apt(Annotation Processing Tool) 解析自動解析。需要做的:
1惨远、自定義類繼承自 AbstractProcessor
2、重寫其中的 process 函數(shù)
其實(shí)就是 apt(Annotation Processing Tool) 在編譯時自動查找所有繼承自 AbstractProcessor 的類话肖,然后調(diào)用他們的 process 方法去處理北秽。

ButterKnife中繼承AbstractProcessor的是ButterKnifeProcessor類:

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

 @Override 
 public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<String>();
    /** 添加支持掃描的注解類型 **/
    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<TypeElement, BindingClass> targetClassMap = findAndParseTargets(env);

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

      try {
        /** 通過bindingClass寫進(jìn)文件,從而生成輔助類最筒。**/
        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;
  }

來看findAndParseTargets()方法:

private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<TypeElement, BindingClass>();
    Set<String> erasedTargetNames = new LinkedHashSet<String>();

    // Process each @Bind element.
    /** 解析每個@Bind元素 **/
    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.
    /** 解析每個監(jiān)聽器方法 **/
    for (Class<? extends Annotation> listener : LISTENERS) {
      findAndParseListener(env, listener, targetClassMap, erasedTargetNames);
    }

    // Process each @BindBool element.
    /** 解析每個@BindBool元素 **/
    for (Element element : env.getElementsAnnotatedWith(BindBool.class)) {
      try {
        parseResourceBool(element, targetClassMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindBool.class, e);
      }
    }
   /** 解析@BindColor **/
   /** 解析@BindDimen **/
   // ……
}

我們來看parseBind()方法:

private void parseBind(Element element, Map<TypeElement, BindingClass> targetClassMap,
      Set<String> erasedTargetNames) {
    // Verify common generated code restrictions.
    /**
      * isInaccessibleViaGeneratedCode()中驗(yàn)證了:
      * 1贺氓、修飾符不能為private或static;2床蜘、不能用于非Class類辙培;3、當(dāng)前類修飾符不能為private
      *
      * isBindingInWrongPackage()驗(yàn)證了邢锯,注解Class不能位于Android framework package或Java framework package扬蕊。
      */
    if (isInaccessibleViaGeneratedCode(Bind.class, "fields", element)
        || isBindingInWrongPackage(Bind.class, element)) {
      return;
    }

    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.ARRAY) {
      /** Array類型 **/
      parseBindMany(element, targetClassMap, erasedTargetNames);
    } else if (LIST_TYPE.equals(doubleErasure(elementType))) {
      /** //list類型,@Bind({ R.id.consume_checkbox, R.id.expired_checkbox, R.id.latest_push_checkbox}) List<CheckedTextView> checkedTextViews; **/
      parseBindMany(element, targetClassMap, erasedTargetNames);
    } else if (isSubtypeOfType(elementType, ITERABLE_TYPE)) {
      /** java.lang.Iterable<?>的子類型 **/
      error(element, "@%s must be a List or array. (%s.%s)", Bind.class.getSimpleName(),
          ((TypeElement) element.getEnclosingElement()).getQualifiedName(),
          element.getSimpleName());
    } else {
      /** 解析單個@Bind **/
      parseBindOne(element, targetClassMap, erasedTargetNames);
    }
  }

我們先來看parseBindOne()方法:

private void parseBindOne(Element element, Map<TypeElement, BindingClass> targetClassMap,
      Set<String> 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();
    }
    /** 必須為view類型的子類或者是接口 **/
    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. 
    /** 只能有一個資源id丹擎。**/
    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<FieldViewBinding> iterator = viewBindings.getFieldBindings().iterator();
        if (iterator.hasNext()) {/** 當(dāng)前資源id已經(jīng)綁定過 **/
          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 {
      /** 從緩存中獲取或創(chuàng)建BindingClass **/
      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());
  }

然后進(jìn)入getOrCreateTargetClass()方法:

private BindingClass getOrCreateTargetClass(Map<TypeElement, BindingClass> targetClassMap,
      TypeElement enclosingElement) {
    /** 從緩存中獲取 **/
    BindingClass bindingClass = targetClassMap.get(enclosingElement);
    if (bindingClass == null) {/** 為空尾抑,則創(chuàng)建 **/
      String targetType = enclosingElement.getQualifiedName().toString();
      String classPackage = getPackageName(enclosingElement);
      /** 生成的輔助類名為:className + "$$ViewBinder" **/
      String className = getClassName(enclosingElement, classPackage) + SUFFIX;

      bindingClass = new BindingClass(classPackage, className, targetType);
      /** 放入緩存 **/
      targetClassMap.put(enclosingElement, bindingClass);
    }
    return bindingClass;
  }

上面主要是解析了注解中的@Bind(其他流程類似),然后放到targetClassMap中蒂培。

一開始我們分析知道最終是通過BindingClass寫入文件再愈,生成輔助類的,那么我們接著來看BindingClass:

String brewJava() {
    StringBuilder builder = new StringBuilder();
    builder.append("http:// Generated code from Butter Knife. Do not modify!\n");
    builder.append("package ").append(classPackage).append(";\n\n");

    if (!resourceBindings.isEmpty()) {
      builder.append("import android.content.res.Resources;\n");
    }
    if (!viewIdMap.isEmpty() || !collectionBindings.isEmpty()) {
      builder.append("import android.view.View;\n");
    }
    builder.append("import butterknife.ButterKnife.Finder;\n");
    if (parentViewBinder == null) {
      builder.append("import butterknife.ButterKnife.ViewBinder;\n");
    }
    builder.append('\n');

    builder.append("public class ").append(className);
    builder.append("<T extends ").append(targetClass).append(">");

    /** parentViewBinder不為空則繼承parentViewBinder护戳,為空則實(shí)現(xiàn)ViewBinder **/
    if (parentViewBinder != null) {
      builder.append(" extends ").append(parentViewBinder).append("<T>");
    } else {
      builder.append(" implements ViewBinder<T>");
    }
    builder.append(" {\n");
    /** 生成綁定方法 **/
    emitBindMethod(builder);
    builder.append('\n');
    /** 生成解綁方法 **/
    emitUnbindMethod(builder);

    builder.append("}\n");
    return builder.toString();
  }

這樣就生成了我們在第一篇中所看到的MainActivity$$ViewBinder.java類:

import android.view.View;
import butterknife.ButterKnife.Finder;
import butterknife.ButterKnife.ViewBinder;

public class MainActivity$$ViewBinder<T extends com.spirittalk.rxjavatraining.MainActivity> implements ViewBinder<T> {
  @Override public void bind(final Finder finder, final T target, Object source) {
    View view;
    view = finder.findRequiredView(source, 2131492970, "field 'mButton' and method 'clickButton'");
    target.mButton = finder.castView(view, 2131492970, "field 'mButton'");
    view.setOnClickListener(
      new butterknife.internal.DebouncingOnClickListener() {
        @Override public void doClick(
          android.view.View p0
        ) {
          target.clickButton();
        }
      });
  }

  @Override public void unbind(T target) {
    target.mButton = null;
  }
}

轉(zhuǎn)載請標(biāo)明出處:http://www.reibang.com/p/4c38616af3a5

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末践磅,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子灸异,更是在濱河造成了極大的恐慌府适,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肺樟,死亡現(xiàn)場離奇詭異檐春,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)么伯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門疟暖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人田柔,你說我怎么就攤上這事俐巴。” “怎么了硬爆?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵欣舵,是天一觀的道長。 經(jīng)常有香客問我缀磕,道長缘圈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任袜蚕,我火速辦了婚禮糟把,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘牲剃。我一直安慰自己遣疯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布凿傅。 她就那樣靜靜地躺著缠犀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪狭归。 梳的紋絲不亂的頭發(fā)上夭坪,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天戈二,我揣著相機(jī)與錄音立莉,去河邊找鬼臀蛛。 笑死昂利,一個胖子當(dāng)著我的面吹牛诽里,可吹牛的內(nèi)容都是我干的籽腕。 我是一名探鬼主播丑勤,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼稳强,長吁一口氣:“原來是場噩夢啊……” “哼敷待!你這毒婦竟也來了间涵?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤榜揖,失蹤者是張志新(化名)和其女友劉穎勾哩,沒想到半個月后抗蠢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡思劳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年迅矛,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片潜叛。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡秽褒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出威兜,到底是詐尸還是另有隱情销斟,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布椒舵,位于F島的核電站蚂踊,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏逮栅。R本人自食惡果不足惜悴势,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望措伐。 院中可真熱鬧特纤,春花似錦、人聲如沸侥加。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽担败。三九已至昔穴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間提前,已是汗流浹背吗货。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留狈网,地道東北人宙搬。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像拓哺,于是被迫代替她去往敵國和親勇垛。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354

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