手撕ButterKnife

思維導圖

使用方法

最新版本具體信息根據(jù)ButterKnife的官網(wǎng)來進行查找猾愿。

  1. 導入包既们。app下的build.gradledependencies中進行引入,當然高版本也容易出現(xiàn)問題。
implementation 'com.jakewharton:butterknife:10.2.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
  1. 項目中進行使用迫悠。根據(jù)官網(wǎng)中給出的使用方式來使用即可,下方只給出一種使用
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的一個注解,將布局中一個控件和引用進行相關聯(lián)的綁定操作全蝶。這樣的操作還有很多

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

不得不承認,ButterKnife在一定的程度上會提高我的開發(fā)效率,但是他到底是怎么運作呢抑淫?

源碼分析

在使用ButterKnife的時候其實我們是否注意到一個問題绷落,我們一定需要寫一個這樣的一段代碼。

ButterKnife.bind(this);

如果不寫會出現(xiàn)下方這樣的錯誤始苇。

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

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

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

            }
        });
    }
報錯信息

我們能夠發(fā)現(xiàn)沒有加入這句的話的代碼出現(xiàn)對象為空的情況砌烁,那我們也就能明白ButterKnife的入口其實就是我們必須要寫的這一段代碼了。

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調(diào)用的函數(shù)
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    // 去尋找一個構造函數(shù)
    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) {
      // 一些錯誤處理
    }
  }

先經(jīng)過上述代碼中的注釋2,也就是使去構造一個對象荣月。如果沒有找到管呵,就直接返回為空;如果找到構造方法了哺窄,就進行構造(使用的ClassLoader來加載捐下,也就是反射機制)。那么主要任務還是注釋3通過newInstance函數(shù)來完成一個Unbinder對象的創(chuàng)建萌业。

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);這段函數(shù)中傳入的泛型正是繼承自Unbinder的啤握,所以我們的泛型返回值也就確定了。

加載文件長相

看看我們通過這個ButterKnife生成的代碼是長什么樣的晶框。

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }
  // 綁定排抬,這里存在兩個函數(shù)是不是似曾相識呢?
  @UiThread
  public MainActivity_ViewBinding(MainActivity target, View source) {
    this.target = target;
    // 通過變量來調(diào)用他內(nèi)部的一個變量
    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;
  }
}

在這里其實我們已經(jīng)明白了授段,為什么我們的變量只能是public或者不加修飾符的原因了蹲蒲。

但是我們并不只是來看這個的,我們要知道注釋的功能是如何實現(xiàn)的侵贵?我們看到了一個我們定義的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();
      }
    });
  }

和我們寫的方式還是一模一樣的乞娄。那我們的好奇心來了瞬逊,他是如何實現(xiàn)這個代碼的輸出的显歧,這也是我們整個ButterKnife的核心工作了。

文件生成過程

其實在最開始的導包的時候确镊,我們就應該注意到的一個問題士骤,因為我們導入的ButterKnife并不是只有一個庫,而我們上面的那個庫的工作明顯是一個調(diào)用的過程蕾域。那猜測一下另外一個庫的作用會不會是生成的作用呢拷肌?

annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'

這里我們要經(jīng)歷一個測試,他是通過ButterKinife.bind(this);觸發(fā)的還是@BindView這一類的注解來進行觸發(fā)的旨巷?
測試之后巨缘,能夠發(fā)現(xiàn)ButterKinife.bind(this);刪除,對我們的使用完全沒有影響契沫,但是刪去全部的@BindView的注解后,文件沒了N艉骸P竿颉!
這也就說明了文件生成的觸發(fā)的方式來自于了注解靶病。而注解所在的包的位置正是庫com.jakewharton:butterknife-compiler中会通。但是到底是哪個文件呢?我們只好一個個看過去了娄周。

文件也不多涕侈,那我們可以一個個看了。首先我們要明確一個目標煤辨,當然這是我的一個猜測裳涛,他應該要對注解進行一個收集,然后再進行一個源碼的生成众辨,而且這個文件中端三,可能會出現(xiàn)幾個如下的特征:
(1)輸出的時候會出現(xiàn)一個后綴"_ViewBinding"。
(2)文件路徑應該會出現(xiàn)對我們自己的包一個名字獲取鹃彻,也就是獲取包名/getPackageName()等獲取函數(shù)郊闯。
(3)編譯時就要調(diào)用的一個注解

ButterKnifeProcessor中我們發(fā)現(xiàn)了一個注解@AutoService(Processor.class)說明了這個文件,而這個注解就是為了編譯時進行加載的蛛株,那我們也就找到了我們的目標了团赁。

  • init()函數(shù)是他的一個入口。
@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()函數(shù)是一個執(zhí)行過程谨履,主要就是一個文件的輸出欢摄。
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    // 調(diào)用一些parseXXX的函數(shù),來獲取注解并生成相對應的數(shù)據(jù)
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

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

      // 這個文件中會出現(xiàn)相對應的文件操作方式
      // 比如上述的一些猜測_ViewBinding后綴的文件創(chuàng)建
      // 獲取包名等一系列操作了笋粟。
      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文件锹引。

總結

在這里就能解決我們的思考的問題了,其實文章中已經(jīng)解決了大部分的問題唆香,剩下最后一個反射的問題嫌变,在這里做一個解答。

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;
    }
    // 躬它。腾啥。。冯吓。通過反射機制創(chuàng)建倘待。
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
}

這是ButterKnife中一段代碼,貼出他的意思很明確组贺,其實就是為了說明會出現(xiàn)反射機制凸舵,對性能也確實有一定的消耗,但是這種消耗并不大失尖,因為他做了一個緩沖的機制啊奄,也就保障我們的性能還是能夠做到較大的緩存的。從編碼效率提高的角度來看掀潮,這種性能代價并不大菇夸。

以上就是我的學習成果,如果有什么我沒有思考到的地方或是文章內(nèi)存在錯誤仪吧,歡迎與我分享庄新。


相關文章推薦:
手撕OkHttp
手撕AsyncTask
HandlerThread那些事兒
手撕Handler

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市薯鼠,隨后出現(xiàn)的幾起案子择诈,更是在濱河造成了極大的恐慌,老刑警劉巖出皇,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吭从,死亡現(xiàn)場離奇詭異,居然都是意外死亡恶迈,警方通過查閱死者的電腦和手機涩金,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來暇仲,“玉大人步做,你說我怎么就攤上這事∧胃剑” “怎么了全度?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長斥滤。 經(jīng)常有香客問我将鸵,道長勉盅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任顶掉,我火速辦了婚禮草娜,結果婚禮上,老公的妹妹穿的比我還像新娘痒筒。我一直安慰自己宰闰,他們只是感情好,可當我...
    茶點故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布簿透。 她就那樣靜靜地躺著移袍,像睡著了一般。 火紅的嫁衣襯著肌膚如雪老充。 梳的紋絲不亂的頭發(fā)上葡盗,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天,我揣著相機與錄音啡浊,去河邊找鬼觅够。 笑死,一個胖子當著我的面吹牛虫啥,可吹牛的內(nèi)容都是我干的蔚约。 我是一名探鬼主播奄妨,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼涂籽,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了砸抛?” 一聲冷哼從身側響起评雌,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎直焙,沒想到半個月后景东,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡奔誓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年斤吐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片厨喂。...
    茶點故事閱讀 40,424評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡和措,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蜕煌,到底是詐尸還是另有隱情派阱,我是刑警寧澤,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布斜纪,位于F島的核電站贫母,受9級特大地震影響文兑,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜腺劣,卻給世界環(huán)境...
    茶點故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一绿贞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧誓酒,春花似錦樟蠕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至歼冰,卻和暖如春靡狞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背隔嫡。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工甸怕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人腮恩。 一個月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓梢杭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親秸滴。 傳聞我的和親對象是個殘疾皇子武契,可洞房花燭夜當晚...
    茶點故事閱讀 45,435評論 2 359