手撕ButterKnife

思惟導圖

使用方法

最新版本具體信息根據ButterKnife的官網來進行查找。java

  1. 導入包。app下的build.gradledependencies中進行引入,固然高版本也容易出現問題。
implementation 'com.jakewharton:butterknife:10.2.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
複製代碼
  1. 項目中進行使用。 根據官網中給出的使用方式來使用便可,下方只給出一種使用
class ExampleActivity extends Activity {
  // 經過BindView的一個
  @BindView(R.id.title) TextView title;

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);
  }
}
複製代碼

經過上述的@BindView的一個註解,將佈局中一個控件和引用進行相關聯的綁定操做。這樣的操做還有不少git

ButterKnife中的註解 對應Java代碼
@BindView findViewById()
@BindString getResources().getString()
@OnClick view.setOnClickListener(new View.OnClickListener() {...})

不得不認可,ButterKnife在必定的程度上會提升個人開發效率,可是他究竟是怎麼運做呢?github

源碼分析

在使用ButterKnife的時候其實咱們是否注意到一個問題,咱們必定須要寫一個這樣的一段代碼。緩存

ButterKnife.bind(this);
複製代碼

若是不寫會出現下方這樣的錯誤。app

@BindView(R.id.view) View view;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
// 判斷寫和不寫時的區別
// ButterKnife.bind(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

            }
        });
    }
複製代碼

報錯信息

咱們可以發現沒有加入這句的話的代碼出現對象爲空的狀況,那咱們也就能明白ButterKnife的入口其實就是咱們必需要寫的這一段代碼了。ide

ButterKnife.bind(this)進行追溯

public static Unbinder bind(@NonNull Activity target) {
    // DecoView是Window中一個變量,是根佈局視圖的載體
    // 詳細須要查看Window的惟一子類PhoneWindow
    // Activity和Window綁定,獲取當前的根視圖
    View sourceView = target.getWindow().getDecorView();
    return bind(target, sourceView); // 1
  }

// 由註釋1調用的函數
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    // 去尋找一個構造函數
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass); // 2

    if (constructor == null) {
      // 直接返回爲空
      return Unbinder.EMPTY;
    }

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      return constructor.newInstance(target, source); // 3
    } catch (IllegalAccessException e) {
      // 一些錯誤處理
    }
  }
複製代碼

先通過上述代碼中的註釋2,也就是使去構造一個對象。若是沒有找到,就直接返回爲空;若是找到構造方法了,就進行構造(使用的ClassLoader來加載,也就是反射機制)。那麼主要任務仍是註釋3經過newInstance函數來完成一個Unbinder對象的建立。函數

public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        if (serializationClass == null) {
            return newInstance0(initargs);
        } else {
            return (T) newInstanceFromSerialization(serializationCtor, serializationClass);
        }
    }
複製代碼

這裏的返回值居然是一個泛型,說明咱們以前有說落了什麼?回頭看看,其實咱們就知道了Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);這段函數中傳入的泛型正是繼承自Unbinder的,因此咱們的泛型返回值也就肯定了。源碼分析

加載文件長相

看看咱們經過這個ButterKnife生成的代碼是長什麼樣的。佈局

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }
  // 綁定,這裏存在兩個函數是否是似曾相識呢?
  @UiThread
  public MainActivity_ViewBinding(MainActivity target, View source) {
    this.target = target;
    // 經過變量來調用他內部的一個變量
    target.view = Utils.findRequiredView(source, R.id.view, "field 'view'");
  }
  
  // 解綁
  @Override
  @CallSuper
  public void unbind() {
    MainActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;
    target.view = null;
  }
}
複製代碼

在這裏其實咱們已經明白了,爲何咱們的變量只能是public或者不加修飾符的緣由了。post

可是咱們並不僅是來看這個的,咱們要知道註釋的功能是如何實現的?咱們看到了一個咱們定義的view變量,作了一個Utils.findRequiredView(source, R.id.view, "field 'view'");的操做,咱們姑且進去看看好了。

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

原來他的尋找原理仍是經過findViewById()來完成整個的定位操做的,那ButterKnife的神奇之處也就再也不神奇了。

爲了驗證咱們的想法,我對@OnClick的註解作了一個測試。下方貼出ButterKnife中給出的答案。

@UiThread
  public MainActivity_ViewBinding(MainActivity target, View source) {
    viewSource = source;
    source.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.start();
      }
    });
  }
複製代碼

和咱們寫的方式仍是如出一轍的。那咱們的好奇心來了,他是如何實現這個代碼的輸出的,這也是咱們整個ButterKnife的核心工做了。

文件生成過程

其實在最開始的導包的時候,咱們就應該注意到的一個問題,由於咱們導入的ButterKnife並非只有一個庫,而咱們上面的那個庫的工做明顯是一個調用的過程。那猜想一下另一個庫的做用會不會是生成的做用呢?

annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
複製代碼

這裏咱們要經歷一個測試,他是經過ButterKinife.bind(this);觸發的仍是@BindView這一類的註解來進行觸發的? 測試以後,可以發現ButterKinife.bind(this);刪除,對咱們的使用徹底沒有影響,可是刪去所有的@BindView的註解後,文件沒了!!! 這也就說明了文件生成的觸發的方式來自於了註解。而註解所在的包的位置正是庫com.jakewharton:butterknife-compiler中。可是究竟是哪一個文件呢?咱們只好一個個看過去了。

文件也很少,那咱們能夠一個個看了。首先咱們要明確一個目標,固然這是個人一個猜想,他應該要對註解進行一個收集,而後再進行一個源碼的生成,並且這個文件中,可能會出現幾個以下的特徵: (1)輸出的時候會出現一個後綴"_ViewBinding"。 (2)文件路徑應該會出現對咱們本身的包一個名字獲取,也就是獲取包名/getPackageName()等獲取函數。 (3)編譯時就要調用的一個註解

ButterKnifeProcessor中咱們發現了一個註解@AutoService(Processor.class)說明了這個文件,而這個註解就是爲了編譯時進行加載的,那咱們也就找到了咱們的目標了。

  • init()函數是他的一個入口。
@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) {
    }
  }
複製代碼
  • process()函數是一個執行過程,主要就是一個文件的輸出。
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    // 調用一些parseXXX的函數,來獲取註解並生成相對應的數據
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      BindingSet binding = entry.getValue();

      // 這個文件中會出現相對應的文件操做方式
      // 好比上述的一些猜想_ViewBinding後綴的文件建立
      // 獲取包名等一系列操做了。
      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;
  }
複製代碼

那這裏咱們也就徹底的解釋清楚了文件是怎麼進行生成的。具體的生成過程仍是須要去查看butterknife.compiler包下的BindingSet文件。

總結

在這裏就能解決咱們的思考的問題了,其實文章中已經解決了大部分的問題,剩下最後一個反射的問題,在這裏作一個解答。

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;
    }
    // 。。。。經過反射機制建立。
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
}
複製代碼

這是ButterKnife中一段代碼,貼出他的意思很明確,其實就是爲了說明會出現反射機制,對性能也確實有必定的消耗,可是這種消耗並不大,由於他作了一個緩衝的機制,也就保障咱們的性能仍是可以作到較大的緩存的。從編碼效率提升的角度來看,這種性能代價並不大。

以上就是個人學習成果,若是有什麼我沒有思考到的地方或是文章內存在錯誤,歡迎與我分享。


相關文章推薦:

手撕OkHttp

手撕AsyncTask

HandlerThread那些事兒

手撕Handler

相關文章
相關標籤/搜索