Android ButterKnife 注解框架的使用详解和原理分析

ButterKnife简介

ButterKnife是JakeWharton大神开发的一个开源库,官方对这个库的介绍为:

Butter Knife
Field and method binding for Android views

ButterKnife是一个使用注解方式来为Android中的View视图绑定字段和方法,能通过自动解析注解来搜索资源文件并赋值给Activity中的字段,如使用@BindView,@BindColor替代原生的findViewById,getColor等方法,或者给View视图的监听器绑定方法,如使用@OnClick 替代 setOnClickListener等方法。ButterKnife通过注解方式为我们封装了很多原生操作,我们只需要编写少量的代码就能实现跟原生代码一样的操作,减少了开发者的很多工作,提高了工作效率。

ButterKnife是通过使用注解方式来自动生成模板代码,从而来将Activity中的字段和方法与View绑定在一起。目前ButterKnife支持如下的特性:

  1. 使用@BindView 来代替findViewById 完成View的引用。
  2. 将多个View组合成list或者array,使用actions,setters或者属性来同时操作它们。
  3. 使用@OnClick等注解字段来注解方法,从而来代替监听器中的匿名内部类。
  4. 使用@BindString等注解字段来注解字段,从而来代替Context.getString等获取资源的方式。

这样说很抽象,下面就使用代码来实践说明一下。

Android Studio配置

从外部引入ButterKnife库很简单,只需要在项目的App包中的build.gradle文件中的dependencies中加入依赖即可:

dependencies {
.....//省略
    compile 'com.jakewharton:butterknife:8.5.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'
}

最新版本的8.5.1,如需了解到最新的版本号可以参考Github:
https://github.com/JakeWharton/butterknife

View绑定

我们一般在Activity中的onCreate中使用findViewById方式来引用布局文件中的View对象,而且还需要进行强制类型转换成所声明的变量的类型。当有多个View时,就需要调用多次findViewById方法,代码工作量大,而且也不简洁。ButterKnife使用@BindView(View的ID)注解字段来注解变量,这样ButterKnife就能自动引用布局中的View资源并能转换为所声明的相应的View类型。

    @BindView(R.id.MainActivity_Btn)Button btn;
    @BindView(R.id.MainActivity_Text)TextView text;
    @BindView(R.id.MainActivity_RecyclerView)RecyclerView recyclerView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        }

注意还需要在onCreate方法调用ButterKnife.bind(this)来绑定Activity和ButterKnife。

以前一直以为ButterKnife是使用反射机制的,所以一直对ButterKnife的性能有点担忧。现在才了解到ButterKnife并不是使用反射机制的,而是解析注解字段来自动生成代码来执行查询和绑定的,说到底就是为我们封装了一些代码,减少我们的工作量。具体生成的代码文件可以查看app包中的build-generated-source-apt-debug-包名中的类文件,里面的类文件命名规则为调用ButterKnife的bind方法的class类加上”_ViewBinding”,具体如下:
Android ButterKnife 注解框架的使用详解和原理分析_第1张图片

点击MainActivity_ViewBinding文件进去查看源码,可以看到变量对应我们在MainActivity中声明的View变量一致:
Android ButterKnife 注解框架的使用详解和原理分析_第2张图片

在MainActivity_ViewBinding方法进行变量的初始化和监听绑定:
Android ButterKnife 注解框架的使用详解和原理分析_第3张图片

在其中的代码中可以看到 Utils.findOptionalViewAsType来引用布局中的View并进行类型转换的:

    target.text = Utils.findOptionalViewAsType(source, R.id.MainActivity_Text, "field 'text'", TextView.class);
    target.recyclerView = Utils.findRequiredViewAsType(source, R.id.MainActivity_RecyclerView, "field 'recyclerView'", RecyclerView.class);

对于监听器的绑定,则直接调用了对应的setOnXXXX方法,然后在监听器方法里面调用target即Activity里面的被ButterKnife注解的事件处理方法:

Android ButterKnife 注解框架的使用详解和原理分析_第4张图片

其中的部分源代码:

    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.multiHandleClick(Utils.castParam(p0, "doClick", 0, "multiHandleClick", 0));
      }
    });


    view = Utils.findRequiredView(source, R.id.MainActivity_EditText, "method 'onTextChanged', method 'beforeTextChanged', and method 'afterTextChanged'");
    view2131427424 = view;
    view2131427424TextWatcher = new TextWatcher() {
      @Override
      public void onTextChanged(CharSequence p0, int p1, int p2, int p3) {
        target.onTextChanged(p0, p1, p2, p3);
      }

      @Override
      public void beforeTextChanged(CharSequence p0, int p1, int p2, int p3) {
        target.beforeTextChanged(p0, p1, p2, p3);
      }

      @Override
      public void afterTextChanged(Editable p0) {
        target.afterTextChanged(p0);
      }
    };
    ((TextView) view).addTextChangedListener(view2131427424TextWatcher);

从上面的分析中就可以总结出ButterKnife的原理了,简单来说就是ButterKnife框架解析注解字段,然后自动生成代码来引用View和为View绑定事件监听器。

Resources资源绑定

除了使用@BindView来引用view,ButterKnife还对引用resource资源文件也提供了相应的支持。可以使用@BindBool,@BindColor,@BindDimen,@BindDrawable,@BindInt,@BindString等注解字段来引用预定义的resource资源。
如:

    @BindString(R.string.app_name)String sub;
    @BindColor(R.color.TextColor)int textColor;

这个资源绑定的原理也很简单,可以参考上面的分析,在MainActivity_ViewBinding文件中的MainActivity_ViewBinding方法中可以看到:

    Context context = source.getContext();
    Resources res = context.getResources();
    target.textColor = ContextCompat.getColor(context, R.color.TextColor);
    target.sub = res.getString(R.string.app_name);

NON-Activity绑定

除了可以在Activity中使用 ButterKnife来进行绑定,还可以在Fragment,RecyclerView的Adapter等任意对象上使用,前提是要提供这个对象上绑定的根视图View Root。
使用 ButterKnife.bind(Object,View);方法来绑定对象和ButterKnife,如下为Fragment中的使用:

public class MainFragment extends Fragment{
    @BindView(R.id.MainFragment_Btn)Button btn;
    @BindView(R.id.MainFragment_Text)TextView text;

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

在Adapter中使用ButterKnife来简化ViewHolder模式:

public class ListAdapter extends RecyclerView.Adapter<ListAdapter.ListHolder> {
    private ArrayList data;

    public ListAdapter(ArrayList list){
        this.data=list;
    }

    @Override
    public ListHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new ListHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item,parent,false));
    }

    @Override
    public void onBindViewHolder(ListHolder holder, int position) {
        holder.text.setText("Text: no"+data.get(position));
    }

    @Override
    public int getItemCount() {
        return data.size();
    }

    static class ListHolder extends RecyclerView.ViewHolder{
        @BindView(R.id.MainActivity_ListItem_Btn)Button btn;
        @BindView(R.id.MainActivity_ListItem_Text)TextView text;

        public ListHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this,itemView);
        }
    }
}

View List

对于多个有相同作用的View时,我们可以将这些View组合成一个List或者Array,然后只需调用一个方法就可以对这些View实行同一个操作。
使用@BindViews( { View.ID,View.ID,View.ID…} )方法来初始化,注意有花括号 { }:

    @BindViews({R.id.MainActivity_FirstName,R.id.MainActivity_MiddleName,R.id.MainActivity_LastName})
    List nameTexts;

对同一组中的View实行同一操作可以使用apply方法,首先我们来了解下两个重要的接口 Action和Setter,这两个接口可以指定对View的行为。在Activity中初始化这两个接口:

    static final ButterKnife.Action DISABLE=new ButterKnife.Action() {
        @Override
        public void apply(@NonNull View view, int index) {
            view.setEnabled(false);
        }
    };

    static final ButterKnife.Setter ENABLE=new ButterKnife.Setter() {
        @Override
        public void set(@NonNull View view, Boolean value, int index) {
            view.setEnabled(value);
        }
    };

    static final ButterKnife.Setter ChangeColor=new ButterKnife.Setter() {
        @Override
        public void set(@NonNull TextView view, Integer value, int index) {
            view.setTextColor(value);
        }
    };

可以看到Action和Setter方法的区别,Action接口只能获取到View对象,但是不能传递参数;而Setter接口使用了泛型,可以在实现接口的时候指定需要传递的数据类型,然后在其中的set方法的第二个参数中获取传递参数。
使用apply就可以执行这些定义中的操作了:

        ButterKnife.apply(nameTexts,DISABLE);
        ButterKnife.apply(nameTexts,ENABLE,false);
        ButterKnife.apply(nameTexts,ChangeColor,textColor);

ButterKnife还允许在apply方法中直接使用Android中的属性,例如:

ButterKnife.apply(nameViews, View.ALPHA, 0.0f);

监听绑定

一般我们为View设置监听需要调用View.addOnXXXXXListener或者setOnXXXXXListener方法来绑定事件监听器,这样每次都要新建匿名内部类,而且有时还必须实现监听器接口中我们并不需要使用的事件方法。ButterKnife也对事件监听器绑定提供了支持,只需要使用相应的注解字段,如@OnClick,@OnTextChanged字段,来注解一个已声明的方法,然后ButterKnife就会自动将这个方法与事件监听器绑定在一起。
对于单个View的事件监听方法,方法参数可有可无:

    @OnClick(R.id.MainActivity_Btn)
    public void click(View view){
        Log.e("ButterKnife","Click:"+view.getId());
    }

    @OnClick(R.id.MainActivity_Btn)
    public void click(){
        Log.e("ButterKnife","Click no arguments");
    }

    @OnClick(R.id.MainActivity_Btn)
    public void click(Button button){//指定View类型,会被自动类型转换
        Log.e("ButterKnife","button Click:"+button.getId());
    }

我们也可以在OnClick方法参数中声明多个View.ID,然后就可以对这一组View绑定同一监听器来实现相同的事件处理:

    @OnClick({R.id.MainActivity_FirstName,R.id.MainActivity_MiddleName,R.id.MainActivity_LastName})
    public void multiHandleClick(TextView textView){
        Log.e("ButterKnife","multiHandleClick:"+textView.getId());
    }

在自定义View中,可以不指定View.ID来绑定监听器:

public class FancyButton extends Button {
  @OnClick
  public void onClick() {
    // TODO do something!
  }
}

监听器多事件处理

当一个View的监听器中有多个事件处理函数时,如TextWatch监听器,我们可以指定其中的一个事件处理函数来绑定方法。ButterKnife为每一个方法注解字段都默认绑定了一个事件处理函数,我们可以使用callback 参数来为方法注解字段指定相应的事件处理函数。
例如@OnTextChanged方法注解字段默认绑定的是TextWatch中的onTextChanged事件处理函数,但我们可以使用callback = OnTextChanged.Callback.BEFORE_TEXT_CHANGED来为@OnTextChanged注解绑定beforeTextChanged事件处理函数,使用callback = OnTextChanged.Callback.AFTER_TEXT_CHANGED来为@OnTextChanged注解绑定afterTextChanged事件处理函数:

    @OnTextChanged(R.id.MainActivity_EditText)
    public void onTextChanged(CharSequence s, int start, int before, int count){
        Log.e("onTextChanged",String.valueOf(s));
    }

    @OnTextChanged(value = R.id.MainActivity_EditText,callback = OnTextChanged.Callback.BEFORE_TEXT_CHANGED)
    public void beforeTextChanged(CharSequence s, int start, int count, int after){
        Log.e("beforeTextChanged",String.valueOf(s));
    }

    @OnTextChanged(value = R.id.MainActivity_EditText,callback = OnTextChanged.Callback.AFTER_TEXT_CHANGED)
    public void afterTextChanged(Editable s){
        Log.e("afterTextChanged",String.valueOf(s.toString()));
    }

解除绑定

Fragment跟Activity一样,有自己对应的生命周期。在Fragment的onCreateView方法中绑定View,在onDestroyView将view置为null。当我们在onCreateView中调用ButterKnife.bind(this,view);时,这个方法会返回一个Unbinder实例对象,可以在适当的生命周期回调函数中调用Unbinder的unbind方法来解除View和ButterKnife的绑定,释放资源。

public class MainFragment extends Fragment{
    @BindView(R.id.MainFragment_Btn)Button btn;
    @BindView(R.id.MainFragment_Text)TextView text;
    private Unbinder unbinder;

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

    @Override
    public void onDestroyView(){
        super.onDestroyView();
        unbinder.unbind();
    }
}

防止异常发生

当使用@Bind来引用View和资源或者绑定View事件处理函数时,如果目标view是null就会抛出异常。为了防止异常的发生和创建可选择性的绑定,在变量上使用@Nullable注解,在方法上使用@Optional注解。

    @Nullable @BindView(R.id.MainActivity_Btn)Button btn;
    @Nullable @BindView(R.id.MainActivity_Text)TextView text;

    @Optional
    @OnClick(R.id.MainActivity_Btn)
    public void click(View view){
        Log.e("ButterKnife","Click:"+view.getId());
    }

使用findById

除了使用@BindView来简化findViewById的使用,我们还可以使用ButterKnife.findById来简化在View,Activity或者Dialog中来引用View的代码。ButterKnife使用了泛型来推断返回的View类型并自动进行强制类型转换:

View view = LayoutInflater.from(context).inflate(R.layout.thing, null);
TextView firstName = ButterKnife.findById(view, R.id.first_name);
TextView lastName = ButterKnife.findById(view, R.id.last_name);
ImageView photo = ButterKnife.findById(view, R.id.photo);

参考:
http://jakewharton.github.io/butterknife/

你可能感兴趣的:(Android)