ButterKnife(二): 原理解析篇

在上一篇文章ButterKnife使用篇里面已經詳細介紹過如何使用ButterKnife了,本篇文章將分析它的原理,了解該工具是如何實現綁定控件的. 本文分析基于ButterKnife版本
'com.jakewharton:butterknife:10.2.1'

 @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(bindLayoutID());
        mBind = ButterKnife.bind(this);
        initView(savedInstanceState);
    }

@Override
    protected void onDestroy() {
        super.onDestroy();
        if (mBind != null && mBind != Unbinder.EMPTY) {
            mBind.unbind();
            mBind = null;
        }
    }

 @Override
    protected int bindLayoutID() {
        return R.layout.activity_main;
    }

這是我們使用ButterKnife時所寫的代碼,那么我們就從 ButterKnife.bind(this)入手,去一步步查看源碼

  /**
   * BindView annotated fields and methods in the specified {@link Activity}. The current content
   * view is used as the view root.
   *
   * @param target Target activity for view binding.
   */
  @NonNull @UiThread
  public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();
    return bind(target, sourceView);
  }

/**
   * BindView annotated fields and methods in the specified {@code target} using the {@code source}
   * {@link View} as the view root.
   *
   * @param target Target class for view binding.
   * @param source View root on which IDs will be looked up.
   */
  @NonNull @UiThread
  public static Unbinder bind(@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);
    }
  }

第一個單參數bind(@NonNull Activity target)方法中,獲取了當前window的頂級父View,它是一個FrameLayout,然后調用Unbinder bind(@NonNull Object target, @NonNull View source)方法,這個方法中主要是通過Class類獲取構造器,并獲取一個新的實例.下面我們繼續(xù)查看findBindingConstructorForClass方法和constructor.newInstance(target, source)方法

  @VisibleForTesting
  static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();


@Nullable @CheckResult @UiThread
  private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")
        || clsName.startsWith("androidx.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      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;
  }

首先從Map中獲取該構造對象,如果存在則直接返回對象,如果不存在,則去構造對應的Constructor.使用Map緩存是為了提高重復使用的效率.
如果類名以"android.","java.","androidx."開頭,則直接返回null,否則創(chuàng)建一個新的Class,名字由傳入的clsName+"_ViewBinding",如果我傳入的類名是ButterKnifeFragment,則返回的實例是ButterKnifeFragment_ViewBinding,查找該文件,可以發(fā)現在build中可以找到,如圖:


文件目錄.png

下面具體看看這個類

public class ButterKnifeFragment_ViewBinding implements Unbinder {
  private ButterKnifeFragment target;

  private View view7f070046;

  @UiThread
  public ButterKnifeFragment_ViewBinding(final ButterKnifeFragment target, View source) {
    this.target = target;

    View view;
    view = Utils.findRequiredView(source, R.id.button, "field 'mButton' and method 'onClick'");
    target.mButton = Utils.castView(view, R.id.button, "field 'mButton'", Button.class);
    view7f070046 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.onClick(Utils.castParam(p0, "doClick", 0, "onClick", 0, Button.class));
      }
    });
    target.mEtList = Utils.listFilteringNull(
        Utils.findRequiredViewAsType(source, R.id.et_1, "field 'mEtList'", EditText.class), 
        Utils.findRequiredViewAsType(source, R.id.et_2, "field 'mEtList'", EditText.class), 
        Utils.findRequiredViewAsType(source, R.id.et_3, "field 'mEtList'", EditText.class));
  }

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

    target.mButton = null;
    target.mEtList = null;

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

我在ButterKnifeFragment頁面添加了4個控件(3EditText和一個Button,button綁定了點擊事件,此處只分析button),還記得前面的雙參數bind(@NonNull Object target, @NonNull View source)方法么,就是調用了這個構造方法.它里面有3個函數,一個是findRequiredView,一個是castView,還有一個點擊事件,下面我們一個個查看源碼

  public static View findRequiredView(View source, @IdRes int id, String who) {
    View view = source.findViewById(id);
    if (view != null) {
      return view;
    }
    String name = getResourceEntryName(source, id);
    throw new IllegalStateException("Required view '"
        + name
        + "' with ID "
        + id
        + " for "
        + who
        + " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
        + " (methods) annotation.");
  }

public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
    try {
      return cls.cast(view);
    } catch (ClassCastException e) {
      String name = getResourceEntryName(view, id);
      throw new IllegalStateException("View '"
          + name
          + "' with ID "
          + id
          + " for "
          + who
          + " was of the wrong type. See cause for more info.", e);
    }
  }

在findRequiredView里面,我們看到了熟悉的findViewById方法...最終,還是回歸到了系統(tǒng)的API. castView方法也是調用系統(tǒng)的cast方法..
那么,它是如何實現點擊事件防抖動的呢? 繼續(xù)往下看

/**
 * A {@linkplain View.OnClickListener click listener} that debounces multiple clicks posted in the
 * same frame. A click on one button disables all buttons for that frame.
 */
public abstract class DebouncingOnClickListener implements View.OnClickListener {
  static boolean enabled = true;

  private static final Runnable ENABLE_AGAIN = () -> 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);
}

點擊一次后,讓該控件enabled = false;再調用v.post(ENABLE_AGAIN);恢復控件的狀態(tài).

到此為止,我們了解到ButterKnife底層依舊是使用的findViewById方法,但是我們依舊有個疑問,ButterKnifeFragment_ViewBinding 并非我們自己寫的,它是如何生成的呢?要知道這個就必須了解注解器相關知識了.

回顧我們使用ButterKnife的條件

dependencies {
  implementation 'com.jakewharton:butterknife:10.2.1'
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
  //kapt 'com.jakewharton:butterknife-compiler:10.2.1'  //Kotlin
}

  @BindView(R.id.button1) Button button1;
  @BindView(R.id.button2) Button button2;

ButterKnifeProcessor完成了注解處理的核心邏輯,下面我們來看看它是如何實現的.

@AutoService(Processor.class)
@IncrementalAnnotationProcessor(IncrementalAnnotationProcessorType.DYNAMIC)
@SuppressWarnings("NullAway") // TODO fix all these...
public final class ButterKnifeProcessor extends AbstractProcessor {
    //忽略部分代碼,只查看核心代碼
.......
 @Override public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    String sdk = env.getOptions().get(OPTION_SDK_INT);
    if (sdk != null) {
      try {
        this.sdk = Integer.parseInt(sdk);
      } catch (NumberFormatException e) {
        env.getMessager()
            .printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '"
                + sdk
                + "'. Falling back to API 1 support.");
      }
    }

    debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));

    typeUtils = env.getTypeUtils();
    filer = env.getFiler();
    try {
      trees = Trees.instance(processingEnv);
    } catch (IllegalArgumentException ignored) {
      try {
        // Get original ProcessingEnvironment from Gradle-wrapped one or KAPT-wrapped one.
        for (Field field : processingEnv.getClass().getDeclaredFields()) {
          if (field.getName().equals("delegate") || field.getName().equals("processingEnv")) {
            field.setAccessible(true);
            ProcessingEnvironment javacEnv = (ProcessingEnvironment) field.get(processingEnv);
            trees = Trees.instance(javacEnv);
            break;
          }
        }
      } catch (Throwable ignored2) {
      }
    }
  }


@Override public Set<String> getSupportedOptions() {
    ImmutableSet.Builder<String> builder = ImmutableSet.builder();
    builder.add(OPTION_SDK_INT, OPTION_DEBUGGABLE);
    if (trees != null) {
      builder.add(IncrementalAnnotationProcessorType.ISOLATING.getProcessorOption());
    }
    return builder.build();
  }

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


 @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;
  }
.......   //忽略部分代碼,只查看核心代碼
}

注意宪睹,ButterKnifeProcessor類上使用了@AutoService(Processor.class)注解并繼承了AbstractProcessor ,來實現注解處理器的注冊樱蛤,注冊到 javac 后账忘,在項目編譯時就能執(zhí)行注解處理器了观谦。

我的ButterKnife相關文章

ButterKnife(一): 使用篇
ButterKnife(二): 原理解析篇
ButterKnife(三): ButterKnifeProcessor解析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末顶滩,一起剝皮案震驚了整個濱河市铃诬,隨后出現的幾起案子贿肩,更是在濱河造成了極大的恐慌烦味,老刑警劉巖聂使,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異谬俄,居然都是意外死亡柏靶,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進店門溃论,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屎蜓,“玉大人,你說我怎么就攤上這事钥勋【孀” “怎么了?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵算灸,是天一觀的道長扼劈。 經常有香客問我,道長乎婿,這世上最難降的妖魔是什么测僵? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上捍靠,老公的妹妹穿的比我還像新娘沐旨。我一直安慰自己,他們只是感情好榨婆,可當我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布磁携。 她就那樣靜靜地躺著,像睡著了一般良风。 火紅的嫁衣襯著肌膚如雪谊迄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天烟央,我揣著相機與錄音统诺,去河邊找鬼。 笑死疑俭,一個胖子當著我的面吹牛粮呢,可吹牛的內容都是我干的。 我是一名探鬼主播钞艇,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼啄寡,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了哩照?” 一聲冷哼從身側響起挺物,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎飘弧,沒想到半個月后识藤,有當地人在樹林里發(fā)現了一具尸體,經...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡眯牧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年蹋岩,在試婚紗的時候發(fā)現自己被綠了赖草。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片学少。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖秧骑,靈堂內的尸體忽然破棺而出版确,到底是詐尸還是另有隱情,我是刑警寧澤乎折,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布绒疗,位于F島的核電站,受9級特大地震影響骂澄,放射性物質發(fā)生泄漏吓蘑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望磨镶。 院中可真熱鬧溃蔫,春花似錦、人聲如沸琳猫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽脐嫂。三九已至统刮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間账千,已是汗流浹背侥蒙。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留匀奏,地道東北人辉哥。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像攒射,于是被迫代替她去往敵國和親醋旦。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內容

  • 最近項目不是很忙会放,因為項目用到了butterknife框架饲齐,所以進行了下系統(tǒng)的研究。研究下來呢發(fā)現這個框架真的是吊...
    我小時候真的可狠了閱讀 319評論 0 2
  • ButterKnife 算是一款知名老牌 Android 開發(fā)框架了咧最,通過注解綁定視圖捂人,避免了 findViewB...
    SheHuan閱讀 21,251評論 2 69
  • 博文出處:ButterKnife源碼分析,歡迎大家關注我的博客矢沿,謝謝滥搭! 0x01 前言 在程序開發(fā)的過程中,總會有...
    俞其榮閱讀 2,029評論 1 18
  • 0X0 前言 做過Android開發(fā)的猿類很多都知道ButterKnife這么個東西捣鲸。這個庫可以大大的簡化我們的代...
    knightingal閱讀 759評論 1 10
  • 前言 這個框架大家都是特別熟悉的了瑟匆,JakeWharton大神的作品,項目地址栽惶,怎么用我就不多講了愁溜,可以去參考官方...
    foxleezh閱讀 1,413評論 0 7