1. 程式人生 > >解析ButterKnife實現原理

解析ButterKnife實現原理

大部分Android開發應該都知道@JakeWharton 大神的ButterKnife註解庫,使用這個庫我們可以不用寫很多無聊的findViewById()和setOnClickListener()等程式碼 
ButterKnife專案的主頁在這裡:http://jakewharton.github.io/butterknife/ 簡單介紹一下使用方法:

<code class="hljs java has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">ExampleActivity</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">Activity</span> {</span>
      <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Bind</span>(R.id.title) EditText titleView;
      <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Bind</span>(R.id.subtitle) EditText subtitleView;

      <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">protected</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> <span class="hljs-title" style="box-sizing: border-box;">onCreate</span>(Bundle savedInstanceState) {
       <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span>.onCreate(savedInstanceState);
       setContentView(R.layout.example_activity);
       ButterKnife.bind(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>);
     }
   }</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right: 1px solid rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right: 1px solid rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li></ul>

但是這個庫是如何工作的呢?可能很多人都覺得ButterKnife在bind(this)方法執行的時候通過反射獲取ExampleActivity中所有的帶有@Bind註解的屬性並且獲得註解中的R.id.xxx值,最後還是通過反射拿到Activity.findViewById()方法獲取View,並賦值給ExampleActivity中的某個屬性 
這是一個註解庫的實現方式,比較原始,一個很大的缺點就是在Activity執行時大量使用反射會影響App的執行效能,造成卡頓以及生成很多臨時Java物件更容易觸發GC 
ButterKnife顯然沒有使用這種方式,它用了Java Annotation Processing技術,就是在Java程式碼編譯成Java位元組碼的時候就已經處理了@Bind、@OnClick(ButterKnife還支援很多其他的註解)這些註解了

Java Annotation Processing

Annotation processing 是javac中用於編譯時掃描和解析Java註解的工具 
你可以你定義註解,並且自己定義解析器來處理它們。Annotation processing是在編譯階段執行的,它的原理就是讀入Java原始碼,解析註解,然後生成新的Java程式碼。新生成的Java程式碼最後被編譯成Java位元組碼,註解解析器(Annotation Processor)不能改變讀入的Java 類,比如不能加入或刪除Java方法 
下圖是Java 編譯程式碼的整個過程,可以幫助我們很好理解註解解析的過程: 
這裡寫圖片描述

ButterKnife 工作流程

當你編譯你的Android工程時,ButterKnife工程中ButterKnifeProcessor類的process()方法會執行以下操作: 
開始它會掃描Java程式碼中所有的ButterKnife註解@Bind、@OnClick、@OnItemClicked等 
當它發現一個類中含有任何一個註解時,ButterKnifeProcessor會幫你生成一個Java類,名字類似$$ViewBinder,這個新生成的類實現了ViewBinder介面 
這個ViewBinder類中包含了所有對應的程式碼,比如@Bind註解對應findViewById(), @OnClick對應了view.setOnClickListener()等等 
最後當Activity啟動ButterKnife.bind(this)執行時,ButterKnife會去載入對應的ViewBinder類呼叫它們的bind()方法

一段Java程式碼:

<code class="hljs scala has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">ExampleActivity</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">Activity</span> {</span>
     <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Bind</span>(R.id.user) EditText username;
     <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Bind</span>(R.id.pass) EditText password;

    <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@Override</span> public void onCreate(Bundle savedInstanceState) {
         <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span>.onCreate(savedInstanceState);
         setContentView(R.layout.simple_activity);
         ButterKnife.bind(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>);
         <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// TODO Use fields…</span>
     }

     <span class="hljs-annotation" style="color: rgb(155, 133, 157); box-sizing: border-box;">@OnClick</span>(R.id.submit) void submit() {
     <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// TODO call server…</span>
     }
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right: 1px solid rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right: 1px solid rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li></ul>

編譯成功後,下面的程式碼生成了:

<code class="hljs axapta has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: "Source Code Pro", monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-class" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">class</span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">ExampleActivity</span>$$<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">ViewBinder</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">T</span> <span class="hljs-inheritance" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">extends</span></span> 
        <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">io</span>.<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">bxbxbai</span>.<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">samples</span>.<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">ui</span>.<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">ExampleActivity</span>> <span class="hljs-inheritance" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">implements</span></span> <span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">ViewBinder</span><<span class="hljs-title" style="box-sizing: border-box; color: rgb(102, 0, 102);">T</span>> {</span>

     @Override <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> bind(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">final</span> Finder finder, <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">final</span> T target, Object source) {
          View view;
          view = finder.findRequiredView(source, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">21313618</span>, “field ‘user’”);
          target.username = finder.castView(view, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">21313618</span>, “field ‘user’”);
          view = finder.findRequiredView(source, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">21313618</span>, “field ‘pass’”);
          target.password = finder.castView(view, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">21313618</span>, “field ‘pass’”);
          view = finder.findRequiredView(source, <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">21313618</span>, “field ‘submit’ and method ‘submit’”);
          view.setOnClickListener(
            <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">new</span> butterknife.internal.DebouncingOnClickListener() {
               @Override <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> doClick(android.view.View p0) {
      target.submit();
           }
        });
      }

     @Override <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">public</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">void</span> reset(T target) {
           target.username = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">null</span>;
           target.password = <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">null</span>;
     }
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right: 1px solid rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li></ul><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right: 1px solid rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li></ul>

用一張圖來說明一下: 
這裡寫圖片描述

ButterKnife.bind 執行階段

最後,執行bind方法時,我們會呼叫ButterKnife.bind(this): 
ButterKnife會呼叫findViewBinderForClass(targetClass)載入ExampleActivity$$ViewBinder.java類 
然後呼叫ViewBinder的bind方法,動態注入ExampleActivity類中所有的View屬性和 
如果Activity中有@OnClick註解的方法,ButterKnife會在ViewBinder類中給View設定onClickListener,並且將@OnClick註解的方法傳入其中 
在上面的過程中可以看到,為什麼你用@Bind、@OnClick等註解標註的屬性或方法必須是public或protected的,因為ButterKnife是通過ExampleActivity.this.editText來注入View的 
為什麼要這樣呢?有些注入框架比如roboguice你是可以把View設定成private的,答案就是效能。如果你把View設定成private,那麼框架必須通過反射來注入View,不管現在手機的CPU處理器變得多快,如果有些操作會影響效能,那麼是肯定要避免的,這就是ButterKnife與其他注入框架的不同 
有一點需要注意 
通過ButterKnife來注入View時,ButterKnife有bind(Object, View) 和 bind(View)兩個方法,有什麼區別呢? 
如果你自定義了一個View,比如public class BadgeLayout extends Fragment,那麼你可以可以通過ButterKnife.bind(BadgeLayout)來注入View的 
如果你在一個ViewHolder中inflate了一個xml佈局檔案,得到一個View物件,並且這個View是LinearLayout或FrameLayout等系統自帶View,那麼不是不能用ButterKnife.bind(View)來注入View的,因為ButterKnife認為這些類的包名以com.android開頭的類是沒有註解功能的(-。- 這不是廢話嗎?),所以這種情況你需要使用ButterKnife.bind(ViewHolder,View)來注入View。 
這表示你是把@Bind、@OnClick等註解寫到了這個ViewHolder類中,ViewHolder中的View呢需要從後面那個View中去找, 大概就是這麼個意思 
參考:https://medium.com/@lgvalle/how-butterknife-actually-works-85be0afbc5ab

ButterKnife的特點

方便的處理View的繫結和點選事件 
方便的處理ListView/RecycleView中ViewHolder的繫結事件 
增強程式碼可讀性 
ButterKnife的使用 
在Acitvity中使用 
在onCreate中

ButterKnife.bind(this) 
在變數前添加註解 - @Bind(view.id)

@Bind(R.id.button) 
Button button; 
新增響應函式

@onClick(R.id.button) 
public void submit(){ 
//TODO 

在non-activity中使用,例如Fragment 
在onCreateView中

ButterKnife.bind(this,view) 
在變數前添加註解

@Bind(R.id.title) 
Button start; 
完整程式碼

public class FancyFragment extends Fragment { 
@Bind(R.id.button1) Button button1; 
@Bind(R.id.button2) Button button2;

    @Override 
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
      View view = inflater.inflate(R.layout.fancy_fragment, container, false);
      ButterKnife.bind(this, view);
      // TODO Use fields...
      return view;
    }


在Viewholder中使用 
在ViewHolder的建構函式中

ButterKnife.bind(this,view) 
在變數前添加註解

@Bind(R.id.title) 
TextView title 
完整程式碼如下

static class ViewHolder { 
@Bind(R.id.title) TextView title;

  public ViewHolder(View view) {
        ButterKnife.bind(this, view);
  }    
}

資源繫結 
通過註解的方式,把資源繫結到對應變數上。需要傳入資源的id,並且不同的資源型別要使用不同的註解。

class ExampleActivity extends Activity { 
@BindString(R.string.title) String title; 
@BindDrawable(R.drawable.graphic) Drawable graphic; 
@BindColor(R.color.red) int red; 
@BindDimen(R.dimen.spacer) Float spacer; 
// … 

通過這種方式,就可以把資源直接賦值給變數,從而不再需要初始化。

View列表 
可以一次性將多個views繫結到一個List或陣列中:

@Bind(R.id.first_name,R.id.middle_name,R.id.last_name) 
List nameViews; 
我覺得這個功能一點也不實用。

事件注入 
點選事件注入 
@OnClick(R.id.submit) 
public void sayHi(Button button){ 
button.setText(“Hello”); 

引數是可選的,但如果存在,必須是這個控制元件類或者控制元件類的父類。

多個控制元件具有相同的事件 
@OnClick({ R.id.door1, R.id.door2, R.id.door3 }) 
public void pickDoor(DoorView door) { 
if (door.hasPrizeBehind()) { 
Toast.makeText(this, “You win!”, LENGTH_SHORT).show(); 
} else { 
Toast.makeText(this, “Try again”, LENGTH_SHORT).show(); 


靠傳入函式的引數,判斷是是哪個控制元件觸發的事件。引數的型別必須是這些控制元件的共同父類。 
在Fragment中,由於與Activity的生命週期不同,有時需解綁ButterKnife 
@override 
public void onDestroyView(){ 
super.onDestroyView(); 
ButterKnife.unbind(this); 

重置注入 
ButterKnife.reset(this);