点亮技能之ButterKnife使用及原理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/pihailailou/article/details/81545072

一,写在前面

       这篇文章将介绍ButterKnife框架的一些基本使用,并会介绍ButterKnife的实现原理。由于ButterKnife是以注解的形式被使用,阅读本篇文章需要读者有Java注解相关的知识。对Java注解不太了解的可参考文章Java注解,文章中涉及注解的知识将不再阐述。

本文将包括如下内容:

  • ButterKnife框架背景介绍
  • 为什么要使用ButterKnife
  • ButterKnife的基本使用 
  • ButterKnife原理浅析

二,轻松聊背景

       ButterKnife是Square公司前工程师Jake Wharton,开发的一款Android框架,主要用于替换传统的findViewById获取控件引用,以及setOnClickListener...等设置监听的方法。在离开伟大的Square公司前,Jake Wharton经常被google邀请参加一些技术分享,然后结局嘛~顺利被挖走啦。于2017年正式入职Google,一家更伟大的公司。为啥说square是一家伟大的公司呢,因为如今很多常用的开源框架都是该公司开源的,大大方便了开发者。

三,凭什么使用ButterKnife

       不知道大家有木有发现,在一个项目越来越大界面越来越多时,每当我们写好了界面,就要在Activity里敲那些重复到吐的findViewByid。俺在工作中就遇到领导要求写个测试接口的apk,而且一看接口有十多个,需要写十多个按钮,findViewByid,setOnClickListener十多次,有种写吐的感觉。那么,使用ButterKnife框架就可以不用写findViewById,setOnClickListener这些重复的代码,当然ButterKnife的用处远不止这些。

       对注解的同学一定知道,注解放在一个元素上面就像该元素的一个标签,注解在编译,运行阶段本身没有任何效果。要想注解生效,必须要人为的处理注解,有两种方式:反射提取注解注解处理器生成辅助类。文章Java注解里介绍了如何使用反射提取注解,这里不再阐述,注解处理器在后面讲到实现原理这块会做简单的介绍。我们知道用反射提取注解有个缺点:使用反射“曲线救国”的方式调用方法,不如直接调用方法快。且反射是在运行阶段执行,运行阶段花费太多时间,会消耗应用一定性能。

       也就是说,反射是在运行阶段处理注解对性能灰常不友好,那么能不能将处理注解的操作放在编译阶段呢?答案是,可以,注解处理器就是做的这件事。注解处理器会对整个源码的特定注解(ButterKnife规定使用的注解)进行解析,并将数据存放在Map集合中,最终通过JavaPoet开源库提供的API将集合里数据转化为Java源代码。也就是说,注解处理器生成了Java源码,该Java文件在module特定的目录中(后面会具体介绍),这些Java源码都是一些辅助类,用于帮助开发者完成findViewById等操作。可想而知,ButterKnife框架处理注解是在编译阶段,即采用注解处理器生成一些辅助类,减少性能的损耗

四,ButterKnife的使用

使用ButterKnife就是使用一堆注解,可使用注解包括:BindView,BindViews,BindColor,BindDimen,BindDrawable,BindString,OnClick,onLongClick...等,完整注解参考ButterKnife注解

在使用ButterKnife框架前,需要在app的build.gradle文件里添加一些依赖:

dependencies {
    //...

    
    implementation 'com.jakewharton:butterknife:8.5.1'
    annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'
}

 

Activity中使用ButterKnife

@BindView注解:绑定View

首先来看下面一个activity_main.xml布局文件,代码如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="www.abc.com.butterknife.MainActivity">

    <TextView
        android:id="@+id/text1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:textSize="14sp"
        android:text="@string/textview_name"/>

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/button_name"
        android:layout_below="@+id/text1"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="10dp"/>

    <EditText
        android:id="@+id/edittext"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/button"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="10dp"
        android:text="@string/edittext_name"/>

    <ImageView
        android:id="@+id/iv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:src="@drawable/ic_launcher_background"/>
</RelativeLayout>

以往获取控件的引用,都是在setContentView方法后调用findViewById方法。那使用ButterKnife框架该怎么用呢,直接上代码:

public class MainActivity extends Activity {    

    @BindView(R.id.text1)
    TextView text1;

    @BindView(R.id.button)
    Button button;

    @BindView(R.id.edittext)
    EditText edit_text;

    @BindView(R.id.iv)
    ImageView iv;

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

第4行:定义一个TextView text1,并在该元素上面添加注解@BindView,注解属性的值为R.id.text1;

第19行:在setContentView方法后调用ButterKnife.bind(this);

如上两个步骤,即替代以往TextView text1 = (TextView)findViewById(R.id.text1)。使用框架后不再需要写findViewById,不需要强制向下转型,直接定义一个控件的引用,并在其上添加注解@BindView(...)。

需要注意,如果使用private,static修饰控件的引用,Build->Make Project编译后会报如下错误:

错误: @BindView fields must not be private or static. (www.abc.com.butterknife.MainActivity.text1)

意思是:添加@BindView注解的字段,不可以使用private,static修饰,具体原因会在分析原理时做出解答。

@BindString,@BindColor,@BindDrawable...绑定资源文件

获取一个字符串资源,需要调用res.getString方法;获取颜色资源,需要调用res.getColor方法;获取图片资源,需要调用res.getDrawable方法。使用ButterKnife框架,分别使用注解@BindString,@BindColor,@BindDrawable替换,注解属性值为资源的id。

代码如下:

    @BindString(R.string.app_name)
    String app_name;

    @BindDrawable(R.drawable.ic_launcher_background)
    Drawable d;

    @BindColor(R.color.red)
    int color_red;

同样,在setContentView方法后调用ButterKnife.bind(this)。

@onClick设置控件的监听

以往给一个View添加点击事件的监听,需要调用方法View$setOnClickListener(OnClickListener l),需要创建OnClickListener接口的子类,并重写onClick方法。

使用ButterKnife框架后,代码简化如下:

    @OnClick({R.id.button, R.id.text1})
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button:
                text1.setTextColor(Color.RED);
                break;

            case R.id.text1:
                text1.setTextColor(Color.BLUE);
                break;
            default:
                break;
        }
    }

第2行:方法名onClick可以随意定义,方法内参数可写,可不写,根据需要进行添加,非常灵活。

第1行:在方法上面添加@onClick注解,由于该注解的属性值是int类型的数组,当同时给R.id.button,R.id.text1添加事件监听时,属性的值用大括号包起来,中间用逗号隔开。关于注解的基础知识,可以参考文章Java注解

ViewHolder中使用ButterKnife

ButterKnife不仅可以在Activity里绑定View,还可以在ViewHolder,Fragment中使用。我们知道ViewHolder经常用在ListView,RecyclerView中,用于封装item里控件的引用。

栗子

接下来看一下传统方式,使用ListView+ViewHolder的代码:

private class MyBaseAdapter extends BaseAdapter {

        @Override
        public int getCount() {
            return 10;
        }

        @Override
        public long getItemId(int position) {
            return 0;
        }

        @Override
        public Object getItem(int position) {
            return null;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View v;
            ViewHolder holder = null;
            if (convertView != null) {
                v = convertView;
                holder = ((ViewHolder) v.getTag());
            } else {
                v = View.inflate(getApplicationContext(), R.layout.list_item, null);
                holder = new ViewHolder();
                holder.tv = v.findViewById(R.id.item_text);
                holder.btn = v.findViewById(R.id.item_button);
                v.setTag(holder);
            }
            holder.tv.setText("TextView " + position);
            holder.btn.setText("button " + position);
            return v;
        }
    }

static class ViewHolder {
    TextView tv;
    TextView btn;
}

第28,29行:使用findViewById方法获取控件引用。

采用ButterKnife框架替换后,代码如下:

private class MyBaseAdapter extends BaseAdapter {

        @Override
        public int getCount() {
            return 10;
        }

        @Override
        public long getItemId(int position) {
            return 0;
        }

        @Override
        public Object getItem(int position) {
            return null;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View v;
            ViewHolder holder = null;
            if (convertView != null) {
                v = convertView;
                holder = ((ViewHolder) v.getTag());
            } else {
                v = View.inflate(getApplicationContext(), R.layout.list_item, null);
                holder = new ViewHolder(v);
                v.setTag(holder);
            }
            holder.tv.setText("TextView " + position);
            holder.btn.setText("button " + position);
            return v;
        }
    }

    static class ViewHolder {
        @BindView(R.id.item_text) TextView tv;
        @BindView(R.id.item_button) Button btn;

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

第37,38行:用法同Activity;

第41行:ButterKnife有多个bind的重载方法,查看该bind方法源码:

  @NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    return createBinding(target, source);
  }

第一个参数target,传入的是ViewHolder的实例。第二个参数source,传入需要绑定的View,在这里对应item的布局资源返回的View对象。所以,第27行需要通过ViewHolder的有参构造方法,给ViewHolder注入View对象。

Fragment中使用ButterKnife

那么在Fragment中如何使用ButterKnife框架呢,使用方式跟Activity中一样,区别在于需要调用ButterKnife$bind的重载方法。

代码如下:

public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.frag_res, container, false);
        Unbinder unbinder = ButterKnife.bind(this, view);
        //...
}

第3行,第一个参数target,传入Fragment的实例。第二个参数source,传入fragment对应资源文件返回的View对象。

bind方法返回了一个Unbinder对象,在回调onDestroyView方法时,调用unbind方法释放资源。

代码如下:

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

@BindViews的使用

ButterKnife有很多注解,这里再介绍一个@BindViews注解的使用。当同一类型的多个View,例如两个TextView控件,需要做同样的操作时,可以使用BindViews进行统一管理。

    @BindViews({R.id.frag_text, R.id.frag_text1})
    List<TextView> views;
    private Unbinder unbinder;

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

        ButterKnife.Action<TextView> mAction = new ButterKnife.Action<TextView>() {
            @Override
            public void apply(@NonNull TextView view, int index) {
                view.setTextColor(Color.BLUE);
            }
        };

        ButterKnife.Setter<TextView, Float> mSetter = new ButterKnife.Setter<TextView, Float>() {
            @Override
            public void set(@NonNull TextView view, Float value, int index) {
                view.setAlpha(value);
            }
        };

        //对两个TextView进行同样的操作
        ButterKnife.apply(views, mAction);
        ButterKnife.apply(views, mSetter, 0.2f);
        ButterKnife.apply(views, View.SCALE_X, 2f);
        return view;
    }

第2行:定义一个存储TextView的List集合容器;

第1行:在List集合上添加注解@BindViews,该注解的属性为int类型的数组,可以存放多个TextView的id值。

第10行:创建一个ButterKnife.Action的匿名子类对象,重写apply方法,;

第13行:设置所有的TextView的文字为blue;

第17行:创建一个ButterKnife.Setter的匿名子类对象,重写set方法;

第20行:给所有的TextView设置透明度为value变量的值;

第25,26,27行,apply三个重载方法,查看其源码如下:

  public static <T extends View> void apply(
        @NonNull List<T> list,
        @NonNull Action<? super T> action) {
    for (int i = 0, count = list.size(); i < count; i++) {
      action.apply(list.get(i), i);
    }
  }

  public static <T extends View, V> void apply(
        @NonNull List<T> list,
        @NonNull Setter<? super T, V> setter, 
        V value) {
    for (int i = 0, count = list.size(); i < count; i++) {
      setter.set(list.get(i), value, i);
    }
  }

  public static <T extends View, V> void apply(
        @NonNull List<T> list,
        @NonNull Property<? super T, V> setter, 
        V value) {
    //noinspection ForLoopReplaceableByForEach
    for (int i = 0, count = list.size(); i < count; i++) {
      setter.set(list.get(i), value);
    }
  }

ButterKnife框架提供了三个重载方法,分别支持Action接口,Setter接口,Property实例。

上述代码执行后,两个TextView的文字颜色为blue,透明度为0.2f,x方向比例扩大1倍。

效果如下:


 

五,ButterKnife实现原理浅析

使用一些注解就可以替换findViewById,setOnClickListener这些传统的方式,那ButterKnife框架是如何实现的呢?前面有讲到,ButterKnife框架处理注解是在编译阶段,使用注解处理器生成一些辅助类,帮助开发者完成相关的操作。从前面ButterKnife的基本使用可以知道,注解与Activity产生关联必然是调用了ButterKnife$bind方法的缘故。

查看ButterKnife$bind源码:

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

  private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    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) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InstantiationException e) {
      throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InvocationTargetException e) {
      Throwable cause = e.getCause();
      if (cause instanceof RuntimeException) {
        throw (RuntimeException) cause;
      }
      if (cause instanceof Error) {
        throw (Error) cause;
      }
      throw new RuntimeException("Unable to create binding instance.", cause);
    }
  }

第2行:获取Activity资源文件对应视图的父窗口DecorView,DecorView是一个FrameLayout布局。

第3行:继续调用createBinding方法,第一个参数target是MainActivity的实例,第二个参数source是DecorView对象;

第7行:获取MainActivity的Class实例;

第9行:获取某一个类的构造器对象,后面会继续分析方法findBindingConstructorForClass;

第17行:调用该构造器对应的构造函数,第一个参数为MainActivity的实例,第二个参数为DecorView的实例,并返回一个unBinder接口的子类对象。

继续查看ButterKnife$findBindingConstructorForClass方法的源码:

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();
    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;
  }

第2行:BINDINGS是一个Map集合,key是MainActivity的Class实例,value是前面提到的“某一个类”的构造器实例;

static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();

第5行:如果bindingCtor不为空,则直接在Map集合里取出来,Map集合用于缓存“某一个类”的构造器实例。

注意:只有第一次调用ButterKnife$bind方法才会使用一次反射获取这个构造器实例,当Map集合中有该构造器的缓存时,会直接从缓存中取。前面提到ButterKnife没有使用反射技术,那为啥这里使用了反射技术呢?要知道是大量的反射调用才会损失“可观”的性能,Only once,对性能影响很小。

第8行:检查MainActivity的包名,是否以原生API的android,Java开头;

第13行:拼接一个字符串"包名.MainActivity_ViewBinding",它就是前面提到的“某一个类”;

第15行:获取MainActivity_ViewBinding类的构造器实例,第一个参数为Class类型,第二个参数为View类型;

那么,这个MainActivity_ViewBinding类在哪呢?选择project视图,点击build->generated->source->apt->debug。

构造方法源码如下:

public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  private View view2131165315;

  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  public MainActivity_ViewBinding(final MainActivity target, View source) {
    this.target = target;

    View view;
    view = Utils.findRequiredView(source, R.id.text1, "field 'text1' and method 'onClick'");
    target.text1 = Utils.castView(view, R.id.text1, "field 'text1'", TextView.class);
    view2131165315 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.onClick(p0);
      }
    });
}

public void unbind() {
    MainActivity target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");
    this.target = null;

    target.text1 = null;

    view2131165315.setOnClickListener(null);
    view2131165315 = null;
  }
}

第1行:MainActivity_ViewBinding实现了接口Unbinder,会重写unbind方法,该方法里用于释放对象资源。

第14,15行:继续查看Utils$findRequiredView,Utils$castView源码如下:

  public static View findRequiredView(View source, @IdRes int id, String who) {
    View view = source.findViewById(id);
    if (view != null) {
      return view;
    }

    //...code
  }

  public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
    try {
      return cls.cast(view);
    } catch (ClassCastException e) {
      //...code
    }
  }

第2行:调用了source.findViewById(id),并返回view;

第12行:cls参数传入的TextView.class,于是将View强制向下转型为TextView类型;

看到这里是不是恍然大悟,框架居然是调用findViewById方法,并强制向下转型获取View的引用。ButterKnife只是生成了一个辅助类MainActivity_ViewBinding,该类封装了获取控件引用的过程。

继续回到MainActivity_ViewBinding的构造方法

第14行:就是findViewByid,前面已经分析;

第15行:强制向下转型为TextView,前面已经分析。注意target.text1,如果text1被private修饰,target.text1将出现编译错误。

第20行:设置监听器DebouncingOnClickListener后,onClick方法里调用了target.onClick(p0)。可以发现ButterKnife仍然是使用setOnClickListener设置监听,只是做了一个封装,让开发者不用再写一大堆setOnClickListener等方法。

第28,30,32,33行:注销监听器,释放对象资源;

六,注解处理器

       前面提到,ButterKnife框架生成了一个辅助类MainActivity_ViewBinding,那么该类是如何生成的呢?答案是:注解处理器在编译阶段生了该辅助类。实现一个注解处理器,需要继承抽象类AbstractProcessor,并重写init,getSupportedAnnotationTypes,getSupportedSourceVersion,getSupportedOptions,process五个方法。在process方法中,会对应用源代码中ButterKnife相关的注解进行扫描并解析其内容,将相关数据存放在一个Map集合中,最后使用Java Poet提供的JavaFile类,将集合中数据串接成字符串生成Java文件。

       关于注解处理器,有一篇翻译自国外大神的文章:Java注解处理器(建议用chrome浏览器打开链接)

                                                                                                            O(∩_∩)O

猜你喜欢

转载自blog.csdn.net/pihailailou/article/details/81545072