ButterKnife源码分析(一)

    xiaoxiao2021-03-25  56

    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 @UiThread public 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 @UiThread private 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的源码(如下所示)。我们发现其实两个语句根本是一样的。

    @Nullable public 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)时,就不再用反射了),会影响一点速度。

    转载请注明原文地址: https://ju.6miu.com/read-33551.html

    最新回复(0)