Butterknife 8.8.1源碼解析:

1、本文須要解決的問題

我研究Butterknife源碼的目的是爲了解決如下幾個我在使用過程當中所思考的問題:java

  1. 在不少文章中都提到Butterknife使用編譯時註解技術,什麼是編譯時註解?
  2. 是徹底不調用findViewById()等方法了嗎?
  3. 爲何綁定各類view時不能使用private修飾?
  4. 綁定監聽事件的時候方法命名有限制嗎?

2、初步分析

基於Butterknife 8.8.1版本。 爲了更好地分析代碼,我寫了一個demo: MainActivity.java:android

public class MainActivity extends Activity {

    @BindView(R.id.text)
    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
    }

    @OnClick(R.id.text)
    public void textClick() {
        Toast.makeText(MainActivity.this, "textview clicked", Toast.LENGTH_LONG);
    }
}
複製代碼

咱們從Butterknife.bind()方法,即方法入口開始分析: ButterKnife#bind():git

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

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

代碼仍是比較清晰的,bind()方法的流程:github

  1. 首先獲取當前activity的sourceView,其實就是獲取Activity的DecorView,DecorView是整個ViewTree的最頂層View,包含標題view和內容view這兩個子元素。咱們一直調用的setContentView()方法其實就是往內容view中添加view元素。
  2. 而後調用createBinding() --> findBindingConstructorForClass(),重點是
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
BINDINGS.put(cls, bindingCtor);
複製代碼

按照所寫的代碼,這裏會加載一個MainActivity_ViewBinding類,而後獲取這個類裏面的雙參數(Activity, View)構造方法,最後放在BINDINGS裏面,它是一個map,主要做用是緩存。在下次使用的時候,就能夠從緩存中獲取到:緩存

Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
    if (debug) Log.d(TAG, "HIT: Cached in binding map.");
    return bindingCtor;
}
複製代碼

3、關於編譯時註解

在上面分析過程當中,咱們知道最後咱們會去加載一個MainActivity_ViewBinding類,而這個類並非咱們本身編寫的,而是經過編譯時註解(APT - Annotation Processing Tool)的技術生成的。 這一節將會介紹一下這個技術。app

一、什麼是註解

註解其實很常見,好比說Activity自動生成的onCreate()方法上面就有一個@Override註解 框架

image.png

  • 註解的概念: 可以添加到 Java 源代碼的語法元數據。類、方法、變量、參數、包均可以被註解,可用來將信息元數據與程序元素進行關聯。
  • 註解的分類:
    • 標準註解,如Override, Deprecated,SuppressWarnings等
    • 元註解,如@Retention, @Target, @Inherited, @Documented。當咱們要自定義註解時,須要使用它們
    • 自定義註解,表示本身根據須要定義的 Annotation
  • 註解的做用:
    • 標記,用於告訴編譯器一些信息
    • 編譯時動態處理,如動態生成java代碼
    • 運行時動態處理,如獲得註解信息
二、運行時註解 vs 編譯時註解

通常有些人提到註解,廣泛就會以爲性能低下。可是真正使用註解的開源框架卻不少例如ButterKnife,Retrofit等等。因此註解是好是壞呢? 首先,並非註解就等於性能差。更確切的說是運行時註解這種方式,因爲它的原理是java反射機制,因此的確會形成較爲嚴重的性能問題。 可是像Butterknife這個框架,它使用的技術是編譯時註解,它不會影響app實際運行的性能(影響的應該是編譯時的效率)。 一句話總結:ide

  • 運行時註解就是在應用運行的過程當中,動態地獲取相關類,方法,參數等信息,因爲使用java反射機制,性能會有問題;
  • 編譯時註解因爲是在代碼編譯過程當中對註解進行處理,經過註解獲取相關類,方法,參數等信息,而後在項目中生成代碼,運行時調用,其實和直接運行手寫代碼沒有任何區別,也就沒有性能問題了。 這樣咱們就解決了第一個問題。
三、如何使用編譯時註解技術

這裏要藉助到一個類:AbstractProcessor函數

public class TestProcessor extends AbstractProcessor {    
    @Override  
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {  
        // TODO Auto-generated method stub 
        return false;  
    }    
}  
複製代碼

重點是process()方法,它至關於每一個處理器的主函數main(),能夠在這裏寫相關的掃描和處理註解的代碼,他會幫助生成相關的Java文件。後面咱們能夠具體看一下Butterknife中的使用。post

4、進一步分析MainActivity_ViewBinding

咱們瞭解了編譯時註解的基本概念以後,咱們先看一下MainActivity_ViewBinding類具體實現了什麼。 在編寫完demo以後,須要先build一下項目,以後能夠在build/generated/source/apt/debug/包名/下面找到這個類,如圖所示:

接上面的分析,到最後會經過反射的方式去調用MainActivity_ViewBinding的構造方法。咱們直接看這個類的構造方法:

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

    View view;
    // 1
    view = Utils.findRequiredView(source, R.id.text, "field 'textView' and method 'textClick'");
    // 2
    target.textView = Utils.castView(view, R.id.text, "field 'textView'", TextView.class);
    // 3
    view2131165290 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
        @Override
        public void doClick(View p0) {
            target.textClick();
        }
    });
}
複製代碼
一、findRequiredView()
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.");
  }
複製代碼

看到這裏咱們已經解決了第二個問題:到最後仍是會調用findViewById()方法,並無徹底捨棄這個方法,這裏的source表明着在上面代碼中傳入的MainActivity的DecorView。你們能夠嘗試一下將Activity轉化爲Fragment的狀況~

二、Util.castView

在這裏,咱們解決了第三個問題,綁定各類view時不能使用private修飾,而是須要用public或default去修飾,由於若是採用private修飾的話,將沒法經過對象.成員變量方式獲取到咱們須要綁定的View。 Util#castView():

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);
    }
}
複製代碼

這裏直接調用Class.cast強制轉換類型,將View轉化爲咱們須要的view(TextView)。

三、
view2131165290 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
    @Override
    public void doClick(View p0) {
        target.textClick();
    }
});
複製代碼

這裏會生成一個成員變量來保存咱們須要綁定的View,重點是下面它會調用setOnClickListener()方法,傳入的是DebouncingOnClickListener:

/** * 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 = new Runnable() {
        @Override public void run() {
            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);
}
複製代碼

這個DebouncingOnClickListener是View.OnClickListener的一個子類,做用是防止必定時間內對view的屢次點擊,即防止快速點擊控件所帶來的一些不可預料的錯誤。我的認爲這個類寫的很是巧妙,既完美解決了問題,又寫的十分優雅,一點都不臃腫。 這裏抽象了doClick()方法,實現代碼中是直接調用了target.textClick(),這裏解決了第四個問題:綁定監聽事件的時候方法命名是沒有限制的,不必定須要嚴格命名爲onClick,也不必定須要傳入View參數。

5、MainActivity_ViewBinding的生成

上文提到,MainActivity_ViewBinding類是經過編譯時註解技術生成的,咱們找到Butterknife相關的繼承於AbstractProcessor的類,ButterKnifeProcessor,咱們直接看process()方法:

public final class ButterKnifeProcessor extends AbstractProcessor {
    @Override 
    public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
        // 1
        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;
    }
}
複製代碼

一、findAndParseTargets() 這個方法的做用是處理全部的@BindXX註解,咱們直接看處理@BindView的部分:

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    // 省略代碼
    // 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);
        }
    }
    // 省略代碼
}

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();
    if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
        if (elementType.getKind() == TypeKind.ERROR) {
            note(element, "@%s field with unresolved type (%s) "
                + "must elsewhere be generated as a View or interface. (%s.%s)",
                BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
        } else {
            error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
                  BindView.class.getSimpleName(), qualifiedName, simpleName);
            hasError = true;
        }
    }

    if (hasError) {
        return;
    }

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

    BindingSet.Builder builder = builderMap.get(enclosingElement);
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    if (builder != null) {
      String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
      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 = simpleName.toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);

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

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

代碼邏輯是處理獲取相關注解的信息,好比綁定的資源id等等,而後經過獲取BindingSet.Builder類的實例來建立一一對應的關係,這裏有一個判斷,若是builderMap存在相應實例則直接取出builder,不然經過getOrCreateBindingBuilder()方法生成一個新的builder,最後調用builder.addField()方法。

後續的話返回到findAndParseTargets()方法的最後一部分:

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    // bindView() 

    // 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;
}
複製代碼

這裏會生成一個bindingMap,key爲TypeElement,表明註解元素類型,value爲BindSet類,經過上述的builder.build()生成,BindingSet類中存儲了不少信息,例如綁定view的類型,生成類的className等等,方便咱們後續生成java文件。最後回到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, debuggable);
      try {
        javaFile.writeTo(filer);
      } catch (IOException e) {
        error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
      }
    }

    return false;
}
複製代碼

最後經過brewJava()方法生成java代碼。 這裏使用到的是javapoet。javapoet是一個開源庫,經過處理相應註解來生成最後的java文件,這裏是項目地址傳送門,具體技術再也不分析。

這篇文章會同步到個人我的日誌,若有問題,請你們踊躍提出,謝謝你們!

相關文章
相關標籤/搜索