ButterKnife源码分析(一)
来源:互联网 发布:java中long转换成date 编辑:程序博客网 时间:2024/05/16 07:09
ButterKnife源码分析(一)
简介
上篇文章介绍了ButterKnife的使用方法(ButterKnife使用详解跳转链接),这篇文章分析一下ButterKnife的源码。算是ButterKnife的代码阅读笔记。
- 一句话介绍:他可以让你省去让人厌烦的findViewById,借助插件可以实现自动绑定Java和XML
- 源码网址:https://github.com/JakeWharton/butterknife
- 最新版本:8.5.1(最新版本的代码与老代码还是有很大变化的,不过好在基本思路没变)
- 本文内容:本文将分析ButterKnife的源码,让我们看看ButterKnife是怎么通过注解实现findViewById的
目录
- ButterKnife源码分析一
- 简介
- 目录
- 举个栗子
- 问题一 BindView是如何生效的
- 问题二 onClick是如何转换的
- 问题三 注解Optional的作用
- 问题四 ButterKnife在Fragment中有什么不同
- 问题四 为什么注解的View不能是private
- 问题六 unBind干了些什么
- 问题五 ButterKnife的类结构是什么样子的呢
- 问题七 MainActivity_ViewBinding是如何生成的呢
- 总结
举个栗子
要看ButterKnife的源码我们先要找一个切入点,给自己提几个问题。带着问题去看源码,效率会高很多哦。
下面我们来举一个最简单的butterKnife的使用例子。
假设有一个:MainActivity.java
public class MainActivity extends AppCompatActivity { @BindView(R.id.tv_content) TextView tvContent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); }}
对应的xml文件为:activity_main.xml。
<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/tv_content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" /></RelativeLayout>
看完这个例子后,我们的第一个问题来了。
问题一: BindView是如何生效的
换句话说,我们想知道ButterKnife是如何将代码
@BindView(R.id.tv_content)TextView tvContent;
变换成代码
TextView tvContent = findViewById(R.id.tv_content)
首先我们从代码
ButterKnife.bind(this);
入手
@NonNull @UiThreadpublic static Unbinder bind(@NonNull Activity target) { View sourceView = target.getWindow().getDecorView(); return createBinding(target, sourceView);}
代码首先会通过target.getWindow().getDecorView();
语句获取activity的跟视图decorView。读过view源码的同学知道,activity的根view为名叫mDecor的Framlayout,也就是这里获取到的sourceView。
获取到sourceView后,将target(就是MainActivity对象)与sourceView(MainActivity的跟视图)作为参数createBinding。
这里我们通过返回值可以猜测一下。通过createBinding获取到的是继承了Unbinder接口的一个实例。
再看createBinding(target, sourceView)方法
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) { //拿到MainActivity.class Class<?> targetClass = target.getClass(); if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName()); //通过targetClass获取一个构造器 Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass); if (constructor == null) { return Unbinder.EMPTY; } //noinspection TryWithIdenticalCatches Resolves to API 19+ only type. try { //使用构造器构造一个实例,参数是target(MainActivity)和source(MainActivity对应的根视图) 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); }}
方法挺长不过,我们真正关心的主要是两句:
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
通过MainActivity.class获取对应的构造器,看到这里我们可以猜到是通过反射创建Unbinder实例。
果然紧接着就是
return constructor.newInstance(target, source);
但究竟是如何将MainActivity与Unbinder对应的呢?继续看findBindingConstructorForClass(targetClass)
@Nullable @CheckResult @UiThreadprivate static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) { //首先从BINDINGS获取Constructor,BINDINGS可以理解为缓存,已经用过的Constructor都会记录在BINDINGS中 Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls); //如果从BINDINGS找到了Constructor直接返回 if (bindingCtor != null) { if (debug) Log.d(TAG, "HIT: Cached in binding map."); return bindingCtor; } String clsName = cls.getName(); //通过类名如果发现是android类文件或者java类文件,则返回空。这里可能奇怪为什么会遇到android或者java文件?我们带着疑问继续看 if (clsName.startsWith("android.") || clsName.startsWith("java.")) { if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search."); return null; } try { //通过反射获取到名叫MainActivity_ViewBinding的类的构造器 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) { //如果MainActivity_ViewBinding这个类没有找到就获取MainActivity.class的父类,继续执行 findBindingConstructorForClass,这里是个递归,这个递归什么时候停止呢?知道遇到android或者java类文件。 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); } //最后将这个Constructor放到缓存中,这样再次用到MainActivity_ViewBinding时就可以不用反射,可以提高运行效率 BINDINGS.put(cls, bindingCtor); return bindingCtor;}
findBindingConstructorForClass方法是一个比较核心的方法,我们来总结一下。这个方法的作用就是利用MainActivity.class的类名,找到名叫MainActivity_ViewBinding的类,并创建一个实例。
所以我们可以把原来MainActivity的方法改一下,这里ButterKnife经过不懈努力,将语句ButterKnife.bind(this)
替换成了new MainActivity_ViewBinding(this, getWindow().getDecorView());
两句话的效果是一样的,不信可以试试,哈哈。
public class MainActivity extends AppCompatActivity { @BindView(R.id.tv_content) TextView tvContent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //ButterKnife.bind(this); new MainActivity_ViewBinding(this, getWindow().getDecorView()); }}
继续提问:MainActivity_ViewBinding在哪里?又是什么样子的呢?我们可以搜索一下这个类
看一下这个类里都有什么
// Generated code from Butter Knife. Do not modify!package com.example.admin.butterknifedemo;import android.support.annotation.CallSuper;import android.support.annotation.UiThread;import android.view.View;import android.widget.TextView;import butterknife.Unbinder;import butterknife.internal.Utils;import java.lang.IllegalStateException;import java.lang.Override;public class MainActivity_ViewBinding implements Unbinder { private MainActivity target; @UiThread public MainActivity_ViewBinding(MainActivity target) { this(target, target.getWindow().getDecorView()); } @UiThread public MainActivity_ViewBinding(MainActivity target, View source) { this.target = target; target.tvContent = Utils.findRequiredViewAsType(source, R.id.tv_content, "field 'tvContent'", TextView.class); } @Override @CallSuper public void unbind() { MainActivity target = this.target; if (target == null) throw new IllegalStateException("Bindings already cleared."); this.target = null; target.tvContent = null; }}
这个类有两个构造方法,和一个unbind函数。我们先看构造方法。不知道大家还记得么,构造方法的两个参数:target(MainActivity) , source (MainActivity的根视图)。 构造方法只有一个方法findRequiredViewAsType,我们看下这个方法做了什么:
public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who, Class<T> cls) { View view = findRequiredView(source, id, who); return castView(view, id, who, cls); }
调用了两个方法,先看findRequiredView:
public static View findRequiredView(View source, @IdRes int id, String who) { View view = source.findViewById(id); if (view != null) { return view; } String name = getResourceEntryName(source, id); throw new IllegalStateException("Required view '" + name + "' with ID " + id + " for " + who + " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'" + " (methods) annotation."); }
终于找到了findViewById。。。。哈哈,可以看到,函数的返回值是View,我们需要的是TextView,所以第二个函数castView出场了,这个函数的作用就是类型强制转换。
所以经过了这一系列的转换。我们的MainActivity又可以改一下了。
public class MainActivity extends AppCompatActivity { TextView tvContent; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tvContent = (TextView) getWindow().getDecorView().findViewById(R.id.tv_content); }}
经过ButterKnife再一次的不懈努力,我们成功的将ButterKnife.bind()
转换成了:
tvContent = (TextView) getWindow().getDecorView().findViewById(R.id.tv_content);
这里可能有人会问,我们一般在Activity中不会写getWindow().getDecorView().findViewById(R.id.tv_content)
这么麻烦,只会写findViewById(R.id.tv_content)
,这里大家可以看下findViewById的源码(如下所示)。我们发现其实两个语句根本是一样的。
@Nullablepublic View findViewById(@IdRes int id) { return getDecorView().findViewById(id);}
问题二: @onClick是如何转换的
我们改一下MainActivity:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); } @OnClick(R.id.tv_content) public void onContentClick() { }}
编译一下,看看生成的 MainActivity_ViewBinding:
@UiThread public MainActivity_ViewBinding(final MainActivity target, View source) { this.target = target; View view; view = Utils.findRequiredView(source, R.id.tv_content, "method 'onContentClick'"); view2131427414 = view; view.setOnClickListener(new DebouncingOnClickListener() { @Override public void doClick(View p0) { target.onContentClick(); } }); }
这里不难理解,只是为什么是DebouncingOnClickListener
不是View.OnClickListener
。看看代码:
/** * A {@linkplain View.OnClickListener click listener} that debounces multiple clicks posted in the * same frame. A click on one button disables all buttons for that frame. */public abstract class DebouncingOnClickListener implements View.OnClickListener { static boolean enabled = true; private static final Runnable ENABLE_AGAIN = new Runnable() { @Override public void run() { enabled = true; } }; @Override public final void onClick(View v) { if (enabled) { enabled = false; v.post(ENABLE_AGAIN); doClick(v); } } public abstract void doClick(View v);}
通过注释不难理解,这个类的作用是当一个Button被按下是,其他的Button都disable。
问题三: 注解@Optional的作用
还是上面的例子
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); } //注意这里增加了@Optional注解 @Optional @OnClick(R.id.tv_content) public void onContentClick() { }}
看下生成的代码
@UiThread public MainActivity_ViewBinding(final MainActivity target, View source) { this.target = target; View view; view = source.findViewById(R.id.tv_content); if (view != null) { view2131427414 = view; view.setOnClickListener(new DebouncingOnClickListener() { @Override public void doClick(View p0) { target.onContentClick(); } }); } }
比之前多了个view空判断。
问题四: ButterKnife在Fragment中有什么不同。
这里我们要看下ButterKnife.bind()方法,之前我们只取了一个例子。这里我们看一下全家福
//适用于activity,view注入 public static Unbinder bind(@NonNull Activity target){ View sourceView = target.getWindow().getDecorView(); return createBinding(target, sourceView); } //在view中绑定子view使用 public static Unbinder bind(@NonNull View target){ return createBinding(target, target); } //适用于Dialog,view注入 public static Unbinder bind(@NonNull Dialog target) { View sourceView = target.getWindow().getDecorView(); return createBinding(target, sourceView); } //适用于在Activity外部使用Activity的view public static Unbinder bind(@NonNull Object target, @NonNull Activity source) { View sourceView = source.getWindow().getDecorView(); return createBinding(target, sourceView); } //适用于Fragment中,view注入 public static Unbinder bind(@NonNull Object target, @NonNull View source) { return createBinding(target, source); } //在dialog外部使用Dialog的view public static Unbinder bind(@NonNull Object target, @NonNull Dialog source) { View sourceView = source.getWindow().getDecorView(); return createBinding(target, sourceView); }
如果大家觉得我写的不够清楚,可以直接看源码注释。
问题四: 为什么注解的View不能是private
我们发现用注解修饰的变量,和方法都不能试private,这是为什么呢,我们可以看一下,MainActivity_ViewBinding的包名,与MainActivity是相同的。而 MainActivity_ViewBinding使用了MainActivity中的tvContent变量和onContentClick方法,如果设置成private会导致编译出错。所以不能试private的。在生成MainActivity_ViewBinding时会逐一检查注解修饰的所有变量和方法是否是private。这部分将在下一篇文章中讲述。
@BindView(R.id.tv_content)TextView tvContent;@OnClick(R.id.tv_content)public void onContentClick() {}
问题六: unBind干了些什么
public void unbind() { MainActivity target = this.target; if (target == null) throw new IllegalStateException("Bindings already cleared."); this.target = null; target.tvContent = null;}
主要是将一些view置空,跟上一篇同样的结论,感觉这样做法稍稍有点多余,也许一些特殊的需求会用到吧。
问题五: ButterKnife的类结构是什么样子的呢
呵呵,本文所讲述的源码,除去注解之外一共只有四个类和一个接口,大家看以仔细看一下,代码还是很简单,很清晰的。8.5.1的代码明显比7.0.1版本的代码清晰了很多。
问题七: MainActivity_ViewBinding是如何生成的呢。
这个将放在下一篇博客介绍,原谅我,头都大了,今天就到这吧。
总结
总结一下,ButterKnife的作用就是将ButterKnife.bind(this)转换成了,我们数据的findViewById,所以下次见到ButterKnife.bind(this)时,大脑可以自动将他认为是很多的view自动finderViewById。
另外在整个过程中一次ButterKnife.bind(this)会使用1次反射(同一个类,第二次调用ButterKnife.bind(this)时,就不再用反射了),会影响一点速度。
- ButterKnife源码分析(一)
- ButterKnife源码分析(二)
- ButterKnife源码分析
- ButterKnife源码分析
- ButterKnife源码分析
- ButterKnife源码分析
- ButterKnife源码分析
- ButterKnife源码分析
- ButterKnife源码分析
- ButterKnife源码分析二
- ButterKnife源码分析
- butterknife 源码分析
- butterknife 源码分析
- ButterKnife源码分析
- Butterknife源码分析
- butterknife源码分析:代码分析
- Android Butterknife 框架源码解析(3)——Butterknife 8.7.0源码分析
- ButterKnife源码研究一宏观
- pycharm编辑器编写Python程序
- Qt 编译时出现“-1: error: LNK2019: unresolved external symbol "public: void __thiscall M”错误
- 《深入浅出Node.js》读书笔记+个人思考(二):异步I/O
- fp in scala 学习随记(1)
- 【nginx】负载配置
- ButterKnife源码分析(一)
- SQL语句大全
- 数位dp, Beautiful numbers
- ZOJ1516-Uncle Tom's Inherited Land
- 算法训练 寻找数组中最大值
- Linux Mysql5.7 安装
- 界面设置笔记
- i=i++的过程?
- (51nod)1046