ButterKnife源碼分析

butterknife注解框架相信很多同學都在用,但是你真的了解它的實現原理嗎?那么今天讓我們來看看它到底是怎么實現的(注:本文是基于butterknife:8.5.1版本進行分析)。

前言

先來看看一些預備知識

java注解

java有三類注解,通過元注解@Retention來標識:

  • RetentionPolicy.SOURCE:源碼級別解析,例如@Override,@SupportWarnngs,這里注解在編譯成功后就不會再起作用,并且不會出現在.class中。

  • RetentionPolicy.CLASS:編譯時解析,默認的解析方式,會保留在最終的class中,但無法再運行時獲取。

  • RetentionPolicy.RUNTIME:運行時注解,會保留在最終的class中,這類注解可以用反射API中getAnnotations()獲取到。

編譯時注解

編譯時注解是注解強大的地方之一,你可以用它來幫你生成java代碼去處理一些邏輯,避免了用反射解析所帶來的性能的開銷。ButterKnife的核心思想正是用編譯時注解。

ButterKnife解析

從Bind開始

眾所周知,使用butterknife時首先需要在Activity#onCreate方法中添加這么一句代碼

ButterKnife.bind(this);

那么我們就從此方法入手,以下是Butterknife.bind()的源碼:

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

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

    try {
      return constructor.newInstance(target, source);
    } catch (IllegalAccessException 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();
    //...... 省略
    try {
      Class<?> bindingClass = Class.forName(clsName + "_ViewBinding");
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
      
    } catch (ClassNotFoundException e) {
      //...... 省略
    }
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
}

首先,bind方法中調用了createBinding(...)方法,而在createBinding()方法中得到了一個Unbinder的構造器,并實例化此類。可以發現這個Unbinder的構造器是通過findBindingConstructorForClass這個方法獲取的,那就進入此方法中瞧瞧。可以看到此方法中首先從BINDINGS中獲取unbinder類構造器,如果有的話直接返回,沒有則通過反射得到此類構造器,先在BINDINGS中存放一份,然后在將其返回。其實這里BINDINGS的作用是緩存通過反射得到的類。避免多次通過反射獲取所帶來的性能開銷。

bind方法暫時就分析到這里,其實要是查看ButterKnife這個類會發現,bind方法有很多重載方法,其中有針對View的,有針對Dialog的等等作用都一樣,就是要利用傳入的target來實例化一個類名為target_ViewBinding類的對象。那么target_ViewBinding這個類是從哪里來的呢?作用有是什么呢?

@BindView(id)注解

使用butterknife的好處是不用程序員手動findViewById去找對應的View,而只需增加一個@BindView(id)即可,那么butterknife是怎么通過@BindView注解來將對應的View初始化的呢?

讓我們先來看看此注解的代碼:

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

通過以上代碼可以看到,BindView注解是一個編譯時注解(@Retention(CLASS)),并且只能作用在屬性上(@Target(FIELD))。

如果你對java注解有一定的了解,那你就一定知道,編譯時注解最終都是有AbstractProcessor的子類來處理的,那我們就找到對應的類為ButterKnifeProcessor類。

先來看看process方法:

@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);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
  }

這個方法有兩個作用,第一是解析所有注解findAndParseTargets(env),第二是生成java代碼。

  1. 解析所有注解findAndParseTargets(env)
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)) {
      try {
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e);
      }
    }
   //省略代碼...
    return bindingMap;
}

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

    //省略代碼 驗證 1、被注解的屬性不能是private或static。2、驗證被注解的屬性必須是View的子類

    // Assemble information on the field.
    int id = element.getAnnotation(BindView.class).value();

    BindingSet.Builder builder = builderMap.get(enclosingElement);
    if (builder != null) {
      String existingBindingName = builder.findExistingBindingName(getId(id));
      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 {
      builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }

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

    //將被注解的屬性封裝成FieldViewBinding存儲在buildSet中
    builder.addField(getId(id), new FieldViewBinding(name, type, required));

    // 將綁定變量所在的類添加到待unBind序列中。
    erasedTargetNames.add(enclosingElement);
  }

這部分代碼很容易懂,解析所有被@BindView所注解的Element(肯定是Field),并處理它。具體解析代碼在parseBindVIew方法中。首先校驗屬性的合法性:

  • 被注解的屬性不能是private或static
  • 被注解的屬性必須是View的子類
  • 屬性與id必須一一對應

其次是將屬性封裝成FieldViewBinding對象,存儲在BindingSet中。那么BindingSet又是個什么玩意呢?

static final class Builder {
    private final TypeName targetTypeName;
    private final ClassName bindingClassName;
    private BindingSet parentBinding;
    private final Map<Id, ViewBinding.Builder> viewIdMap = new LinkedHashMap<>();
    //省略代碼
    void addField(Id id, FieldViewBinding binding) {
      getOrCreateViewBindings(id).setFieldBinding(binding);
    }

    private ViewBinding.Builder getOrCreateViewBindings(Id id) {
      ViewBinding.Builder viewId = viewIdMap.get(id);
      if (viewId == null) {
        viewId = new ViewBinding.Builder(id);
        viewIdMap.put(id, viewId);
      }
      return viewId;
    }

    BindingSet build() {
      //省略代碼      
      return new BindingSet(targetTypeName, bindingClassName, isFinal, isView, isActivity, isDialog,
          viewBindings.build(), collectionBindings.build(), resourceBindings.build(),
          parentBinding);
    }
  }

這里貼出來了BindingSet的構造器Builder,可以看出此類包裝了被注解的類的信息以及注解的所有的屬性,viewIdMap中存儲了所有的注解字段,這些字段都被封裝成ViewBinding(其實就是將id和屬性名包裝成一個對象)。最后由BindingSet負責生成java代碼。

static Builder newBuilder(TypeElement enclosingElement) {
    //省略代碼
    String packageName = getPackage(enclosingElement).getQualifiedName().toString();
    String className = enclosingElement.getQualifiedName().toString().substring(
        packageName.length() + 1).replace('.', '$');
    ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");

    boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
    return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
}

可以看到構造Builder時會傳入包名,類名,而這個類名 className_ViewBinding就是最終被生成的類的名稱。還記得在bind方法中會通過反射去實例化一個target_ViewBinding類的對象吧,這個類其實就是這里通過注解來自動生成的。到這里butterknife的原理就分析完了,下面我們來看看如何生成java代碼。

2.使用javapoet生成java代碼

再次介紹一個大殺器 javapoet,square出品的生成.java文件的Java API。通過清晰的語法來生成一個文件結構,無敵。
那么我們回到process方法中查看。

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

這里主要邏輯在binding.brewJava()方法中。

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

這里builder的參數第一個是包名。第二個是TypeSpec對象,就是要生成的類的信息。

private TypeSpec createType(int sdk) {

    //獲取一個builder對象,將類的修飾符設置為public,如果允許是final時,設置為final
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
        .addModifiers(PUBLIC);
    if (isFinal) {
      result.addModifiers(FINAL);
    }

    ......
    //添加屬性
    if (hasTargetField()) {
      result.addField(targetTypeName, "target", PRIVATE);
    }

    //添加構造器
    if (isView) {
      result.addMethod(createBindingConstructorForView());
    } else if (isActivity) {
      result.addMethod(createBindingConstructorForActivity());
    } else if (isDialog) {
      result.addMethod(createBindingConstructorForDialog());
    }
    if (!constructorNeedsView()) {
      // Add a delegating constructor with a target type + view signature for reflective use.
      result.addMethod(createBindingViewDelegateConstructor());
    }
    result.addMethod(createBindingConstructor(sdk));

    //添加一個unbind方法
    if (hasViewBindings() || parentBinding == null) {
      result.addMethod(createBindingUnbindMethod(result));
    }

    return result.build();
  }

在這個方法中可以看到最終生成的類的相關信息,最主要的給屬性初始化的代碼是在createBindingConstructor(sdk) 這個方法中。

private MethodSpec createBindingConstructor(int sdk) {
    MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
        .addAnnotation(UI_THREAD)
        .addModifiers(PUBLIC);
    ......
    //綁定View屬性
    if (hasViewBindings()) {
      ......
      for (ViewBinding binding : viewBindings) {
        addViewBinding(constructor, binding);
      }
      ......
    }
    ......
    return constructor.build();
  }
  
  private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
    if (binding.isSingleFieldBinding()) {
      // Optimize the common case where there's a single binding directly to a field.
      FieldViewBinding fieldBinding = binding.getFieldBinding();
      CodeBlock.Builder builder = CodeBlock.builder()
          .add("target.$L = ", fieldBinding.getName());

      boolean requiresCast = requiresCast(fieldBinding.getType());
      if (!requiresCast && !fieldBinding.isRequired()) {
        builder.add("source.findViewById($L)", binding.getId().code);
      } else {
        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;
    }

    ......

    addFieldBinding(result, binding);
    addMethodBindings(result, binding);
  }

這兩個方法主要就是生成 通過findViewById方法去找到對應的View 的代碼。可以看到注解庫最終還是調用的findViewById來查找View的。

到這里ButterKnife注解庫就分析完了。下面貼一段最終生成的代碼瞧瞧:

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  private View view2131427417;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public MainActivity_ViewBinding(final MainActivity target, View source) {
    this.target = target;

    View view;
    target.txt1 = Utils.findRequiredViewAsType(source, R.id.id1, "field 'txt1'", TextView.class);
    view = Utils.findRequiredView(source, R.id.btn, "method 'start'");
    view2131427417 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.start();
      }
    });
  }

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

    target.txt1 = null;
    
    view2131427417.setOnClickListener(null);
    view2131427417 = null;
  }
}

總結:首先通過注解來獲取對應類中所有要初始化的屬性,通過processor編譯生成對應的.java的類文件。這個類文件中會在構造器中通過調用View的findViewById方法去初始化所有的屬性(注意:到這里只是生成了.java類文件,并沒有和目標類綁定)。這個編譯時生成的類的構造器中需要傳入對應的目標類作為參數。因此在目標類初始化時需要調用ButterKnife.bind(this)方法類進行綁定。在這個bind方法中會通過反射得到編譯時才生成的類的對象,這樣就和目標類進行了綁定,也就是初始化目標類中的被注解的屬性。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,182評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,489評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,290評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,776評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,510評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,866評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,860評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,036評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,585評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,331評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,536評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,058評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,754評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,154評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,469評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,273評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,505評論 2 379

推薦閱讀更多精彩內容

  • 博文出處:ButterKnife源碼分析,歡迎大家關注我的博客,謝謝! 0x01 前言 在程序開發的過程中,總會有...
    俞其榮閱讀 2,062評論 1 18
  • 主目錄見:Android高級進階知識(這是總目錄索引)?前面我們已經講完[編譯期注解的使用例子]大家應該對這個流程...
    ZJ_Rocky閱讀 1,495評論 0 8
  • 引言 在Android開發中我們會用到ButterKnife框架來簡化綁定layout中的視圖。這里我們主要分析B...
    伍零一閱讀 304評論 0 1
  • butterknife是一個Android View和Callback注入框架,相信很多人都在使用,可以減少很多代...
    不二先生的世界閱讀 286評論 0 1
  • 此刻他的思想,已經悄悄地溜到了雙重思想的迷幻世界中去了:明明知道,卻佯裝不知;本來對事實心知肚明,卻偏要費勁心機去...
    木卯丁閱讀 295評論 0 1