1. 程式人生 > >Android開發之手把手教你寫ButterKnife框架(一)

Android開發之手把手教你寫ButterKnife框架(一)

系列文章目錄導讀:

一、概述

JakeWharton我想在Android界無人不知,無人不曉的吧, ButterKnife這個框架就是出自他隻手。這個框架我相信很多人都用過,本系列部落格就是帶大家更加深入的認識這個框架,ButterKnife截至目前已有1w+的star:

這裡寫圖片描述

如果我們對於這個優秀框架還是停留在使用階段,那就太可惜。

本系列文章的主要內容如下:
1,ButterKnife是什麼?
2,ButterKnife的作用和功能介紹。
3,ButterKnife的實現原理。
4,自己動手實現個ButterKnife。

二、ButterKnife是什麼?

ButterKnife是一個編譯時的依賴注入框架(compile-time dependency injection framework)用來簡化android中類似findViewById、setOnclickListener等的模板程式碼。
比如在寫activity介面的時候常常有如下程式碼:

public class MyActivity extents Activity{
    private EditText etConsultValidDate;
    private TextView tvToolbarCenter;
    private TextView  tvLeftAction;
    private
TextView tvRightAction; private TextView tvConsultTip; private TextView tvSuggestTime; private EditText etConsultTitle; private EditText etConsultDesc; private EditText etConsultTime; private EditText etConsultNumber; private RelativeLayout rlContactInfo; private LinearLayout llAnswerTime; private
TextView tvAnswerTime, tvAnswerTimePre; private TextView tvToolbarRight; private LinearLayout llBottom; private int from; private RelativeLayout rlOppositeInfo; private ImageView ivHeadIcon; private TextView tvOppositeUsername, tvOppositeDesc; @Override protected void initViews() { tvExpertIdentify = (TextView) findViewById(R.id.tv_expert_identify); llBottom = (LinearLayout) findViewById(R.id.ll_bottom); rlOppositeInfo = (RelativeLayout) findViewById(R.id.rl_opposite_info); ivHeadIcon = (ImageView) findViewById(R.id.iv_head_icon); tvOppositeUsername = (TextView) findViewById(R.id.tv_opposite_username); tvOppositeDesc = (TextView) findViewById(R.id.tv_opposite_desc); rbOppositeScore = (RatingBar) findViewById(R.id.rbar_star); tvUserCompany = (TextView) findViewById(R.id.tv_user_company); infoArrow = findViewById(R.id.iv_member_info_arrow); tvConsultTip = (TextView) findViewById(R.id.tv_consult_tip); tvLeftAction = (TextView) findViewById(R.id.tv_left_action); tvRightAction = (TextView) findViewById(R.id.tv_right_action); llAnswerTime = (LinearLayout) findViewById(R.id.ll_answer_time); tvAnswerTimePre = (TextView) findViewById(R.id.tv_answer_time_pre); tvAnswerTime = (TextView) findViewById(R.id.tv_answer_time); tvLeftAction.setOnClickListener(this); tvRightAction.setOnClickListener(this); etConsultTitle = (EditText) findViewById(R.id.et_consult_title); etConsultDesc = (EditText) findViewById(R.id.et_consult_desc); etConsultTime = (EditText) findViewById(R.id.et_contact_time); etConsultNumber = (EditText) findViewById(R.id.et_contact_number); etConsultValidDate = (EditText) findViewById(R.id.et_consult_valid_day); tvSuggestTime = (TextView) findViewById(R.id.tv_contact_time); } }

初始化Views大量的沒有技術含量的模板程式碼。如果介面比較複雜的話,這樣的程式碼變得更多。使用ButterKnife可以很好的簡化上面冗長的程式碼。

public class MyActivity extents Activity{

    @BindView(R.id.xxx) EditText etConsultValidDate;
    @BindView(R.id.xxx) TextView tvToolbarCenter;
    @BindView(R.id.xxx) TextView tvLeftAction;
    @BindView(R.id.xxx) TextView tvRightAction;
    @BindView(R.id.xxx) TextView tvConsultTip;
    @BindView(R.id.xxx) TextView tvSuggestTime;
    @BindView(R.id.xxx) EditText etConsultTitle;
    @BindView(R.id.xxx) EditText etConsultDesc;
    @BindView(R.id.xxx) EditText etConsultTime;
    @BindView(R.id.xxx) EditText etConsultNumber;
    @BindView(R.id.xxx) RelativeLayout rlContactInfo;
    @BindView(R.id.xxx) LinearLayout llAnswerTime;
    @BindView(R.id.xxx) TextView tvAnswerTime, tvAnswerTimePre;
    @BindView(R.id.xxx) TextView tvToolbarRight;
    @BindView(R.id.xxx) LinearLayout llBottom;
    @BindView(R.id.xxx) RelativeLayout rlOppositeInfo;
    @BindView(R.id.xxx) ImageView ivHeadIcon;
    @BindView(R.id.xxx) TextView tvOppositeUsername;
    @BindView(R.id.xxx) TextView tvOppositeDesc;

    @Override 
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.simple_activity);
        //初始化Views
        ButterKnife.bind(this); 
    }
}

相比之下,極大的簡化了View的初始化程式碼。

三、ButterKnife的功能介紹(所有的功能)

除了上面的@BindView註解,還其他功能:

1. 使用@BindViews初始化多個View

@BindViews({ R.id.first_name, R.id.middle_name, R.id.last_name })
List nameViews;

2. 使用@OnClick設定監聽事件

@OnClick(R.id.submit)
public void submit(View view) {
// TODO do something…
}

如果不想要submit方法引數可以去掉如:
@OnClick(R.id.submit)
public void submit() {
// TODO do something…
}

View的引數還可以自動轉換,比如給TextView設定點選事件
@OnClick(R.id.submit)
public void submit(TextView textView) {
// TODO do something…
}

如果是自定義的View可以不指定View Id 如:
public class FancyButton extends Button {
@OnClick
public void onClick() {
// TODO do something!
}
}

3. listView item點選事件

@OnItemSelected(R.id.list_view)
void onItemSelected(int position) {
// TODO …
}

4. view的onTouchEvent

@OnTouch(R.id.example) boolean onTouch() {
Toast.makeText(this, “Touched!”, Toast.LENGTH_SHORT).show();
return false;
}

5. 監聽EditText的addTextChangedListener

@OnTextChanged(R.id.example) void onTextChanged(CharSequence text) {
Toast.makeText(this, “Text changed: ” + text, Toast.LENGTH_SHORT).show();
}

6. 設定ViewPager的OnPageChangeListener

@OnPageChange(R.id.example_pager) void onPageSelected(int position) {
Toast.makeText(this, “Selected ” + position + “!”, Toast.LENGTH_SHORT).show();
}

7. 設定TextView的OnEditorActionListener(該事件主要用來設定軟鍵盤上的按鈕)

@OnEditorAction(R.id.example) boolean onEditorAction(KeyEvent key) {
Toast.makeText(this, “Pressed: ” + key, Toast.LENGTH_SHORT).show();
return true;
}

8. 設定View的OnFocusChangeListener事件

@OnFocusChange(R.id.example) void onFocusChanged(boolean focused) {
Toast.makeText(this, focused ? “Gained focus” : “Lost focus”, Toast.LENGTH_SHORT).show();
}

9. 設定View的OnLongClickListener長按事件

@OnLongClick(R.id.example) boolean onLongClick() {
Toast.makeText(this, “Long clicked!”, Toast.LENGTH_SHORT).show();
return true;
}

10. 關於資源的繫結

@BindString(R.string.title) String title; //字串
@BindDrawable(R.drawable.graphic) Drawable graphic; //drawable
@BindColor(R.color.red) int red; // int or ColorStateList field
@BindDimen(R.dimen.spacer) Float spacer; // int (for pixel size) or float (for exact value) field
@BindArray(R.array.countries) String[] countries; 字串陣列
@BindArray(R.array.icons) TypedArray icons;
@BindBool(R.bool.is_tablet) boolean isTablet;

四、ButterKnife的實現原理

通過上面的例子我們知道,要使用ButterKnife首先要在目的碼使用註解,然後在onCreate生命週期方法裡呼叫ButterKnife.bind(this);方法。使用註解沒什麼好說的,那只有看看ButterKnife.bind(this);這個方法是怎麼實現的:

  @NonNull @UiThread
  public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
  }

獲取activity的decorView,然後呼叫createBinding方法:

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    //獲取target的位元組碼
    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) {
        //Ignore Exceptions
    } 
  }

createBinding方法的第一個引數target就是我們的activity例項,source就是decorView。上面的程式碼也比較簡單,我也加上了註釋。這個方法我就不多說了。然後看看findBindingConstructorForClass方法是怎麼實現的:

  @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();
    //如果是android framework裡的類則直接return
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return null;
    }
    try {
      //拼接類名,然後獲取該類的位元組碼
      Class<?> bindingClass = Class.forName(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;
  }

findBindingConstructorForClass方法核心程式碼是下面2行程式碼:

Class<?> bindingClass = Class.forName(clsName + "_ViewBinding");
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);

意思就是target的型別(qualified name)拼接_ViewBinding,然後通過獲取拼接後類的構造方法。那麼clsName + _ViewBinding 這個類是從哪裡來的。
我們把butterKnife的原始碼(8.4.0版本)下載下來,build程式碼後,查詢以_ViewBinding為結尾的java類,發現有十個只有,都位於各自所在專案的build->gernerated->source->apt->debug目錄下。
以裡面的SimpleActivity_ViewBinding為例:

// Generated code from Butter Knife. Do not modify!
package com.example.butterknife.library;
public class SimpleActivity_ViewBinding<T extends SimpleActivity> implements Unbinder {
  //ignore some code
  @UiThread
  public SimpleActivity_ViewBinding(final T target, View source) {
    target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);
    target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class);
    view = Utils.findRequiredView(source, R.id.hello, "field 'hello', method 'sayHello', and method 'sayGetOffMe'");
    target.hello = Utils.castView(view, R.id.hello, "field 'hello'", Button.class);
    //ignore some code
  }
}

target其實是我們上面的activity,source就是DecorView。發現所有的View的初始化工作全部放在了SimpleActivity_ViewBinding構造方法裡。

// Generated code from Butter Knife. Do not modify!

通過這句話我們知道,SimpleActivity_ViewBinding是ButterKnife生成的。那麼ButterKnife是怎麼生成這個類的呢?

通過一個叫APT(Annotation Processing Tool)工具生成對應的類。

總結下:
1. butterKnife是一個執行時依賴注入框架,有效地幫我們簡化一些重複程式碼。
2. butterKnife在ButterKnife.bind方法裡通過反射呼叫對應的類構造方法執行初始化工作,所以butterKnife並不是完全沒有使用反射,只在這個地方用到了。所以butterKnife的效率也是很高的。對於反射這個技術,不應該持極端態度(徹底不用,或到處濫用)。特別是在android中,到處都是反射,對效能也是有一定的影響的。
3. butterknife使用 apt技術來生成java類。

本系列預計有三篇文章,下一篇文章將介紹在android studio中如何使用apt。最後一篇文章講如何實現呼叫bind方法就完成view的初始化工作。