解析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);