ButterKnife源碼解析

ButterKnife想必每一個(gè)Android開發(fā)者都或多或少使用過屋剑,它的功能強(qiáng)大之處就不用多說了扎阶。它的原理可以簡要概括為:編譯時(shí)注解(AbstractProcessor)+反射议慰,網(wǎng)上已經(jīng)有很多ButterKnife源碼解析相關(guān)的文章了仇轻,閑暇之余將ButterKnife工程clone下來又翻了遍源碼水由,當(dāng)作學(xué)習(xí)筆記整理下扬绪。

ButterKnife使用了編譯時(shí)注解竖独,入口就是ButterKnifeProcessor這個(gè)類,它繼承自AbstractProcessor挤牛,在跟進(jìn)去ButterKnifeProcessor源碼前莹痢,我們先簡要概括下注解處理器的概念:

注解處理器(AbstractProcessor)是用來在編譯過程中掃描和處理注解的工具,我們在項(xiàng)目中可以為特定的注解注冊自己的注解處理器墓赴,生成.java 文件竞膳,但不能修改已經(jīng)存在的Java類(即不能向已有的類中添加方法)。而這些生成的Java文件竣蹦,會同時(shí)與其他普通的手寫Java源代碼一起被javac編譯顶猜。若大家有對AbstractProcessor還不太了解的童鞋,請先移步至相關(guān)文章痘括。

好了长窄,我們跟進(jìn)去ButterKnifeProcessor首先先看下它的getSupportedAnnotationTypes方法

@Override public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<>();
    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
      types.add(annotation.getCanonicalName());
    }
    return types;
  }

  //ButterKnife支持的注解類型
  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;
  }

可以看到ButterKnife支持的注解很多,它不但支持我們常用的BindView纲菌、OnClick注解挠日,還支持BindColor、BindDrawable等注解翰舌。接著我們跟進(jìn)去ButterKnifeProcessor的process方法看下:

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //1嚣潜、調(diào)用findAndParseTargets方法,處理所有的@BindXX注解
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    //2椅贱、遍歷bindingMap懂算,生成相應(yīng)的Java文件
    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;
  }

關(guān)鍵部分我在上述代碼中已經(jīng)做了標(biāo)注只冻,我們接著跟進(jìn)去findAndParseTargets方法,在findAndParseTargets方法中處理了所有支持的注解计技,由于該方法有點(diǎn)長喜德,這里我們只關(guān)注下BindView相關(guān)的部分,其他注解原理類似:

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

    ...

    // Process each @BindView element.   
    //1垮媒、處理每個(gè)被@BindView標(biāo)注的元素
    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);
      }
    }
   
    ...

    //2舍悯、調(diào)用BindingSet.Builder的build方法,生成相應(yīng)的BindingSet對象睡雇,并put到bindingMap中
    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, classpathBindings.keySet());
      if (parentType == null) {
        bindingMap.put(type, builder.build());
      } else {
        BindingInformationProvider parentBinding = bindingMap.get(parentType);
        if (parentBinding == null) {
          parentBinding = classpathBindings.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);
        }
      }
    }
    
    //3萌衬、最后將bindingMap  return掉
    return bindingMap;
  }

我們接著跟進(jìn)去1處的parseBindView方法:

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

   一系列校驗(yàn)工作
   ···
   if (hasError) {
      return;
    }

    //1、通過getAnnotation.value()方法它抱,獲取到相應(yīng)控件id秕豫,類似于R.id.btn_test
    int id = element.getAnnotation(BindView.class).value();
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    //2、將int類型的id包裝成Id對象
    Id resourceId = elementToId(element, BindView.class, id);
    if (builder != null) {
      String existingBindingName = builder.findExistingBindingName(resourceId);
      if (existingBindingName != null) {
        error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
            BindView.class.getSimpleName(), id, existingBindingName,
            enclosingElement.getQualifiedName(), element.getSimpleName());
        return;
      }
    } else {
      //3抗愁、創(chuàng)建TypeElement相對應(yīng)的BindingSet.Builder實(shí)例馁蒂,并put到builderMap中
      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);
  }

這里我們關(guān)注下3處呵晚,在調(diào)用getOrCreateBindingBuilder方法創(chuàng)建BindingSet.Builder對象的過程中會聲明編譯生成的.java文件名蜘腌,相見getBindingClassName方法:

static ClassName getBindingClassName(TypeElement typeElement) {
    String packageName = getPackage(typeElement).getQualifiedName().toString();
    String className = typeElement.getQualifiedName().toString().substring(
            packageName.length() + 1).replace('.', '$');
    return ClassName.get(packageName, className + "_ViewBinding");
  }

上述代碼就印證了通過ButterKnife生成的Java文件名為className + "_ViewBinding"的形式。

好了饵隙,我們回到最初的ButterKnifeProcessor撮珠,接著看下process方法的2處,遍歷bindingMap金矛,生成相應(yīng)的Java文件芯急,這里生成Java文件用到了開源庫javapoet。我們跟進(jìn)去brewJava方法看下:

JavaFile brewJava(int sdk, boolean debuggable) {
    //1驶俊、createType方法
    TypeSpec bindingConfiguration = createType(sdk, debuggable);
    return JavaFile.builder(bindingClassName.packageName(), bindingConfiguration)
        .addFileComment("Generated code from Butter Knife. Do not modify!")
        .build();
  }

跟進(jìn)去createType方法:

private TypeSpec createType(int sdk, boolean debuggable) {
    //1娶耍、聲明Java文件的類名、修飾符饼酿、是否final等榕酒,其實(shí)就是開源庫javapoet的API操作
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC)
        .addOriginatingElement(enclosingElement);
    if (isFinal) {
      result.addModifiers(FINAL);
    }
  
    //重點(diǎn):createBindingConstructor 創(chuàng)建構(gòu)造方法
    result.addMethod(createBindingConstructor(sdk, debuggable));
    
    ...

    return result.build();
  }

createBindingConstructor方法中又調(diào)用到了addViewBinding方法,我們跟進(jìn)去看下:

private void addViewBinding(MethodSpec.Builder result, ViewBinding binding, boolean debuggable) {
    if (binding.isSingleFieldBinding()) {
      // Optimize the common case where there's a single binding directly to a field.
      FieldViewBinding fieldBinding = requireNonNull(binding.getFieldBinding());
      CodeBlock.Builder builder = CodeBlock.builder()
          .add("target.$L = ", fieldBinding.getName());
      //標(biāo)記是否需要強(qiáng)轉(zhuǎn)
      boolean requiresCast = requiresCast(fieldBinding.getType());
      if (!debuggable || (!requiresCast && !fieldBinding.isRequired())) {
        if (requiresCast) {
          builder.add("($T) ", fieldBinding.getType());
        }
        //直接拼接findViewById($L)  故俐,$L為占位符想鹰,findViewById括號中正好為相應(yīng)控件id,類似于R.id.btn_test
        builder.add("source.findViewById($L)", binding.getId().code);
      } else {
        //通過Utils類包裝了一層药版,內(nèi)部也是調(diào)用findViewById操作
        builder.add("$T.find", UTILS);
        builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
        if (requiresCast) {
          builder.add("AsType");
        }
        builder.add("(source, $L", binding.getId().code);
        if (fieldBinding.isRequired() || requiresCast) {
          builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
        }
        if (requiresCast) {
          builder.add(", $T.class", fieldBinding.getRawType());
        }
        builder.add(")");
      }
      result.addStatement("$L", builder.build());
      return;
    }

    ...
  }

方法addViewBinding中簡單明了辑舷,就是用來拼接我們在Activity或者Fragment中常寫的findViewById那行代碼。

到這里AbstractProcessor相關(guān)的部分我們就分析完畢了槽片,簡單講就是在代碼編譯期間掃描每個(gè)Java文件中的特定的注解何缓,通過開源庫javapoet來“拼湊”成ViewBinding文件肢础,該Java文件的命名為className + "_ViewBinding"。那么那么中間文件又是在什么時(shí)候被調(diào)用的呢碌廓?答案就是ButterKnife.bind(this);

我們跟進(jìn)去ButterKnife.bind方法看下:

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

接著跟進(jìn)去bind方法:

@NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    //1乔妈、獲取目標(biāo)class
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
   //2、通過反射獲取對應(yīng)_viewBinding類的構(gòu)造方法
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

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

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      //3氓皱、創(chuàng)建構(gòu)造對象路召,執(zhí)行findViewById操作(在編譯時(shí),我們在構(gòu)造方法中“拼接”了findViewById代碼)
      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);
    }
  }

好了波材,ButterKnife源碼分析到這里就結(jié)束了股淡,下一小節(jié)我們手動擼一個(gè)簡易的ButterKnife框架。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末廷区,一起剝皮案震驚了整個(gè)濱河市唯灵,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌隙轻,老刑警劉巖埠帕,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異玖绿,居然都是意外死亡敛瓷,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門斑匪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來呐籽,“玉大人,你說我怎么就攤上這事蚀瘸〗频” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵贮勃,是天一觀的道長贪惹。 經(jīng)常有香客問我,道長寂嘉,這世上最難降的妖魔是什么奏瞬? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮垫释,結(jié)果婚禮上丝格,老公的妹妹穿的比我還像新娘。我一直安慰自己棵譬,他們只是感情好显蝌,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般曼尊。 火紅的嫁衣襯著肌膚如雪酬诀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天骆撇,我揣著相機(jī)與錄音瞒御,去河邊找鬼。 笑死神郊,一個(gè)胖子當(dāng)著我的面吹牛肴裙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播涌乳,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼蜻懦,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了夕晓?” 一聲冷哼從身側(cè)響起宛乃,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎蒸辆,沒想到半個(gè)月后征炼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡躬贡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年谆奥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逗宜。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡雄右,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出纺讲,到底是詐尸還是另有隱情,我是刑警寧澤囤屹,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布熬甚,位于F島的核電站,受9級特大地震影響肋坚,放射性物質(zhì)發(fā)生泄漏乡括。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一智厌、第九天 我趴在偏房一處隱蔽的房頂上張望诲泌。 院中可真熱鬧,春花似錦铣鹏、人聲如沸敷扫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽葵第。三九已至绘迁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間卒密,已是汗流浹背缀台。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留哮奇,地道東北人膛腐。 一個(gè)月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像鼎俘,于是被迫代替她去往敵國和親依疼。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

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