Android(安卓)ButterKnife入门到放弃
世界公认最高效的学习方法 :
-
选择一个你要学习的内容
-
想象如果你要将这些内容教授给一名新人,该如何讲解
-
如果过程中出了问题,重新回顾这个内容
-
简化:容你的讲解越来越简单易懂
————理查德费曼学习法
一、前言
二、简介
三、ButterKnife入门
(1)是什么?
(2)有什么用?
(3)为什么要用?
四、ButterKnife渡劫
(4)怎么用?
五、ButterKnife封神之路
(5)原理是什么?
(6)辅助插件
六、总结
七、内容推荐
八、项目参考
一、前言
以前一直在写一些常用的功能模块,如何使用及封装。后来发现网上一搜一大堆,关键是写得比我好也就算了,更过分的是字数比我还多,简直不让人活了。后来自己就搞了个工具Demo项目,把常用的功能都弄上去。也不打算写这类文章,如果朋友们在开发功能上遇到障碍可以去项目(文章最下面)找找,说不定就有了。
如今只能被逼改行给大家介绍一些Android目前比较火的开源库。可以添加到项目当中,提高开发效率,并让项目开发起来更轻松方便,易与维护。提高逼格.... (又在胡扯)
二、简介
本篇文章要给大家介绍的是最容易使用,也是最简单的ButterKnife。用过的人都知道为什么好用,没有过的也不用后悔,现在学了也不吃亏,花10分钟看完本篇文章就懂了。 (10分钟你买不了吃亏,也买不了上当)
在开始讲之前给大家看看大纲,也许一眼你就学会了也说不一定
本篇围绕几个问题展开:
- 是什么?
- 有什么用?
- 为什么要用?
- 怎么用?
- 原理是什么?
等明白这几个问题后(就可以渡劫了),在给大家介绍个好用的辅助插件。
三、ButterKnife入门
(1)是什么?
一句话:是出自JakeWharton大神开源的一个依赖注入库,通过注解的方式来替代android中view的相关操作。
那么问题来了:什么是依赖注入? 这里简单介绍介绍一下,忘了是哪个博客CP过来的了 。
什么是依赖注入?
依赖注入通俗的理解就是,当A类需要引用B类的对象时,将B类的对象传入A类的过程就是依赖注入。依赖注入最重要的作用就是解耦,降低类之间的耦合,保证代码的健壮性、可维护性、可扩展性。
常用的实现方式有构造函数、set方法、实现接口等。例如:
// 通过构造函数public class A { B b; public A(B b) { this.b = b; }}// 通过set方法public class A { B b; public void setB(B b) { this.b = b; }}
(2)有什么用?
一句话:减少大量的findViewById以及setOnClickListener代码,且对性能的影响较小。
(3)为什么要用?
一句话:使用简单,容易上手、学习成本低、对性能影响小
四、ButterKnife渡劫
(4)怎么用?
1.添加依赖
android { ... // Butterknife requires Java 8. compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation 'com.jakewharton:butterknife:10.2.0' annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0' //若是kotlin 使用kapt 代替annotationProcessor }
如果要在库中使用,将插件添加到buildscript
buildscript { repositories { mavenCentral() google() } dependencies { //classpath 'com.android.tools.build:gradle:3.3.0' classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.0' } }
并在moudle添加这些代码
apply plugin: 'com.android.library'apply plugin: 'com.jakewharton.butterknife'
在库中使用R2代替R
class ExampleActivity extends Activity { @BindView(R2.id.user) EditText username; @BindView(R2.id.pass) EditText password;...}
如果不需要再库中使用 ,直接依赖第一块代码既可
2.绑定布局
①Activity
//Activity中的使用class ExampleActivity extends Activity { @BindView(R.id.title) TextView title; @BindView(R.id.subtitle) TextView subtitle; @BindView(R.id.footer) TextView footer; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.simple_activity); ButterKnife.bind(this); // TODO Use fields... }}
②Fragment
//Fragment使用public class FancyFragment extends Fragment { @BindView(R.id.button1) Button button1; @BindView(R.id.button2) Button button2; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fancy_fragment, container, false); ButterKnife.bind(this, view); // TODO Use fields... return view; }}
③适配器
//适配器中使用public class MyAdapter extends BaseAdapter { @Override public View getView(int position, View view, ViewGroup parent) { ViewHolder holder; if (view != null) { holder = (ViewHolder) view.getTag(); } else { view = inflater.inflate(R.layout.whatever, parent, false); holder = new ViewHolder(view); view.setTag(holder); } holder.name.setText("John Doe"); // etc... return view; } static class ViewHolder { @BindView(R.id.title) TextView name; @BindView(R.id.job_title) TextView jobTitle; public ViewHolder(View view) { ButterKnife.bind(this, view); } }}
④其他对象
//其他类中可以通过View view = inflater.inflate(R.layout.fancy_fragment, null);ButterKnife.bind(this, view);
通过ButterKnife.bind()方法直接跟对象绑定在一起,之后给控件添加注释 表示这个控件已经和对象绑定。就可以直接使用了
3.声明并使用
①绑定控件
//第一种方式:@BindView@BindView(R.id.tv_hell_world)TextView tvHellWorld;//使用方式:tvHellWorld.setText("Hello world");//第二种方式:@@BindViews@BindViews({ R.id.first_name, R.id.middle_name, R.id.last_name })List nameViews;//很遗憾的是最新版已经找不到该方法了 只能当List使用ButterKnife.apply(nameViews, DISABLE);ButterKnife.apply(nameViews, ENABLED, false);static final ButterKnife.Action DISABLE = new ButterKnife.Action() { @Override public void apply(View view, int index) { view.setEnabled(false); }};static final ButterKnife.Setter ENABLED = new ButterKnife.Setter() { @Override public void set(View view, Boolean value, int index) { view.setEnabled(value); }};ButterKnife.apply(nameViews, View.ALPHA, 0.0f);
②绑定资源
//values文件里面的资源绑定 @BindString 声明:@BindString(R.string.title) String title;@BindDrawable 声明: @BindDrawable(R.drawable.graphic) Drawable graphic@BindColor 声明:@BindColor(R.color.red) int red;@BindDimen 声明:@BindDimen(R.dimen.spacer) float spacer;
③绑定监听事件
@OnClick//单个点击事件@OnClick(R.id.submit)public void submit(View view) { // TODO submit data to server...}//多个点击事件@OnClick({R.id.btn_send,R.id.btn_close,R.id.btn_canle}) public void onViewClicked(View view) { switch (view.getId()){ case R.id.btn_send: break; case R.id.btn_close: break; case R.id.btn_canle: break; } }方法参数可变//无参数情况@OnClick(R.id.submit)public void submit() { // TODO submit data to server...}//指定特定类型参数情况@OnClick(R.id.submit)public void sayHi(Button button) { button.setText("Hello!");}
④多种监听器
//选中与未选中监听@OnItemSelected(R.id.list_view)void onItemSelected(int position) { // TODO ...}@OnItemSelected(value = R.id.maybe_missing, callback = NOTHING_SELECTED)void onNothingSelected() { // TODO ...}
这里只介绍比较常用的几个注解,更多注解可以查看源码。 使用方法大同小异
4.解绑
//Fragment具有不同于activity的生命周期,需要在合适的生命周期中进行解绑public class FancyFragment extends Fragment { @BindView(R.id.button1) Button button1; @BindView(R.id.button2) Button button2; private Unbinder unbinder; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fancy_fragment, container, false); unbinder = ButterKnife.bind(this, view); // TODO Use fields... return view; } @Override public void onDestroyView() { super.onDestroyView(); unbinder.unbind(); }}
根据文档提示Fragment具有不同于Activity的生命周期。在onCreateView中绑定一个Fragment时,在onDestroyView中将视图设置为null。当您为您调用bind时,Butter Knife返回一个Unbinder实例。在适当的生命周期回调中调用它的unbind方法
5.可选绑定
①默认情况下,@Bind和listener绑定都是必需的。如果找不到目标视图,将引发异常
②要禁止这种行为并创建可选绑定,请向字段添加@Nullable注释或向方法添加@Optional注释
③使用方式
@Nullable @BindView(R.id.might_not_be_there) TextView mightNotBeThere;@Optional @OnClick(R.id.maybe_missing) void onMaybeMissingClicked() { // TODO ...}
5.注意事项
①ButterKnife.bind(this);必须在setContentView();之后调用;且父类绑定后,子类不需要再绑定
②在非Activity 类(eg:Fragment、ViewHold)中绑定: ButterKnife.bind(this,view);这里的this不能替换成getActivity()
③使用ButterKnife修饰的方法和控件,不能用private or static 修饰,否则会报错。
④文档提示:Fragment中使用绑定时 ,需要再onDestroyView()中进行解绑
6.混淆
最后,别忘了在 proguard-rules.pro 文件中加入混淆代码,确保在混淆后仍可以继续运行。
-keep public class * implements butterknife.Unbinder { public (**, android.view.View); }-keep class butterknife.*-keepclasseswithmembernames class * { @butterknife.* ; }-keepclasseswithmembernames class * { @butterknife.* ; }
看到这里,若还有不会的。建个项目照着步骤来一次,5分钟就可搞定。也是最容易集成的库之一。不试一试怎么知道好不好用。
当学会了使用之后一起来研究一下ButterKnife是怎么实现的。怎么做出来 。
五、ButterKnife封神之路
(5)原理是什么?
1.从开始调用的方法说起,ButterKnife都是从bind()开始执行。先看下ButterKnife里面的bind方法,再来说说bind的作用
// butterKnife 有许多bind重载方法 最终都会传递到 bind(@NonNull Object target, @NonNull View source)方法中 //绑定Actvity @NonNull @UiThread public static Unbinder bind(@NonNull Activity target) {//根据targert获取Activity的根视图 View sourceView = target.getWindow().getDecorView(); return bind(target, sourceView); }//绑定视图 @NonNull @UiThread public static Unbinder bind(@NonNull View target) { return bind(target, target); }//绑定Dialog @NonNull @UiThread public static Unbinder bind(@NonNull Dialog target) { View sourceView = target.getWindow().getDecorView(); return bind(target, sourceView); }//传入的对象与Activity视图绑定一起 @NonNull @UiThread public static Unbinder bind(@NonNull Object target, @NonNull Activity source) { View sourceView = source.getWindow().getDecorView(); return bind(target, sourceView); }//传入的对象与Dialog视图绑定一起 @NonNull @UiThread public static Unbinder bind(@NonNull Object target, @NonNull Dialog source) { View sourceView = source.getWindow().getDecorView(); return bind(target, sourceView); }//调用viewBinding构造函数传入绑定对象与视图最终获取Unbinder对象 @NonNull @UiThread public static Unbinder bind(@NonNull Object target, @NonNull View source) {//获取绑定对象的类 Class<?> targetClass = target.getClass();//打印类名 if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());//通过绑定类获取对应的viewBinding类的构造函数 Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);//判断为空时返回 Unbinder的空实例 if (constructor == null) { return Unbinder.EMPTY; } //noinspection TryWithIdenticalCatches Resolves to API 19+ only type. try { // 通过反射调用了viewBinding的构造函数创建了一个实例 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); } }
——通过bind()方法传入一个对象与视图最终获取Unbinder的一个实例。那Unbinder是什么,用来干嘛呢。我们看下源码
public interface Unbinder { @UiThread void unbind(); Unbinder EMPTY = () -> { };}
——Unbinder其实是个接口,里面有个unbind方法。就是用来在Fragment中进行解绑时用的。还有个空实现,java 8写法。通过上面的注释,在未找到对应的ViewBinding构造函数时调用。
2.bind()方法中有个重要的方法,通过它获取ViewBinding的构造函数。从而实现对ViewBinding类的调用。源码如下
@VisibleForTesting static final Map, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();//通过绑定的类 查找对应的ViewBinding类 并获取ViewBinding类的构造函数 @Nullable @CheckResult @UiThread private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {//ButterKnife定义了一个LinkedHashMap用来存储绑定类和对应的viewBinding构造函数//根据指定类查找对应的构造函数 Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);//构造函数不为空或者存储对象的key含有这个类则返回构造函数 if (bindingCtor != null || BINDINGS.containsKey(cls)) { if (debug) Log.d(TAG, "HIT: Cached in binding map."); return bindingCtor; }//未得到构造函数后 继续向下执行 通过反射获取类名 String clsName = cls.getName();//判断是不是框架类 是则返回null if (clsName.startsWith("android.") || clsName.startsWith("java.") || clsName.startsWith("androidx.")) { if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search."); return null; } try { //通过类加载器 获取 ?_viewBinding类 (这个viewBind类是在项目编译ButterKnife通过APT根据生成的 命名是根据绑定的类名+ViewBinding) Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding"); //noinspection unchecked //通过反射获取?_ViewBinding类的构造方法 bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class); if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor."); } catch (ClassNotFoundException e) { //如果未发现?_ViewBinding类 则获取父类传到该方法里面继续寻找_ViewBinding 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); }// 把?_ViewBinding的构造函数存储在LinkedHashMap中 BINDINGS.put(cls, bindingCtor);// 并返回?_ViewBinding构造函数 return bindingCtor; } //butterKnife其他的方法 //设置是否打印日志 public static void setDebug(boolean debug) { ButterKnife.debug = debug; }
——通过绑定的类利用反射得到类的加载方法,根据指定的名字去查找对应的ViewBinding类。
——通过获取到的ViewBinding类利用反射得到它的一个构造函数,最后通过调用构造函数实例化ViewBinding的一个实现。
通过上面注释,我们可以看到几个方法都是通过反射机制实现的。那么什么是反射呢? 这里简单介绍一下:
反射机制:反射机制允许程序在执行期借助于Reflection API取得任何类的内部信息,并能直接操作任意对象的内部属性及方法
优点:可以实现动态创建对象和编译,体现出很大的灵活性
缺点:对性能有影响,此类操作总是慢于直接执行相同的操作
总结:
- 通过bind()方法传入一个对象与视图
- 通过传入的绑定对象利用反射原理找到对应的viewBinding类并获取到构造函数
- 构造函数通过反射实例化了ViewBinding类并把绑定的对象与视图传到ViewBinding类中
那么问题来了,绑定对象的ViewBinding类是怎么来的呢?(如何生成的?)。这里在简单介绍一下。
首先得了解这几个知识
APT(Android注解处理器):APT(Annotation Processing Tool) 即注解处理器,是一种注解处理工具,用来在编译期扫描和处理注解,通过注解来生成 Java 文件。即以注解作为桥梁,通过预先规定好的代码生成规则来自动生成 Java 文件
原理:在注解了某些代码元素(如字段、函数、类等)后,在编译时编译器会检查 AbstractProcessor 的子类,并且自动调用其 process() 方法,然后将添加了指定注解的所有代码元素作为参数传递给该方法,开发者再根据注解元素获取相应的对象信息。根据这些信息通过 javapoet 生成我们所需要的代码。
通过上面描述总结:
- 当编译代码的时候,APThi扫描和处理注解
- 把所有的注解传递到AbstractProcessor 子类的process()方法中(也就是我们需要自定义一个类继承AbstractProcessor 类并重写process()方法
- 在process()中获取注解信息并使用javapoet技术生成我们需要的文件
那么可能有人会问什么是注解呢:
java注解:又称 Java 标注,是 JDK5.0 引入的一种注释机制。Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容。
看起来有点抽象:不是很了解的这里给大家在推荐个 注解视频资源
JavaPoet:JavaPoet是一个用于生成. Java源文件的Java API。
如果大家理解了这几个知识再来看这篇文章就轻松多了,一眼扫过,就会发现都是废话。作者还挺啰嗦的有木有,觉得是待会点个赞让作者知道下。
写太多篇幅太长,看得也累。所以具体如何生成的还是要靠大家去看。
继续分析生成的ViewBinding类,这里以MainActivity为例
3.MainActivity_ViewBinding源码及注释如下
//1.ButterKnife类最终执行constructor.newInstance(target, source)通过反射调用了viewBinding构造函数方法,这里以MainActivity的ViewBinding的源码进行分析@UiThread public MainActivity_ViewBinding(final MainActivity target, View source) { //把绑定对象赋给viewBinding类的属性,在解绑的时候释放资源 this.target = target; View view;// 这里调用了Utils.findRequiredViewAsType()方法来获取控件 target.tvHellWorld = Utils.findRequiredViewAsType(source, R.id.tv_hell_world, "field 'tvHellWorld'", TextView.class); target.etInput = Utils.findRequiredViewAsType(source, R.id.et_input, "field 'etInput'", EditText.class);//通过@OnClick注释得到id 找到对应的View 并实现点击事件 并执行MainActivity的onViewClicked方法(onViewClicked是在MainActivity中我们自己定义的名字) view = source.findViewById(R.id.btn_send); if (view != null) { view7f070024 = view; view.setOnClickListener(new DebouncingOnClickListener() { @Override public void doClick(View p0) { target.onViewClicked(p0); } }); }//获取资源对象赋值给MainActivity对象 Context context = source.getContext(); Resources res = context.getResources(); target.colorAccent = ContextCompat.getColor(context, R.color.colorAccent); target.helloWorld = res.getString(R.string.helloWorld); } //2.获取控件的方法 public static T findRequiredViewAsType(View source, @IdRes int id, String who, Class cls) {//根据id找到对应的View View view = findRequiredView(source, id, who);//把veiw类型强转成传进来的cls return castView(view, id, who, cls); } //3.内部通过findViewById 获取到View 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."); } //4.把View转成对应的Class类型 public static T castView(View view, @IdRes int id, String who, Class cls) {try { return cls.cast(view);} catch (ClassCastException e) { String name = getResourceEntryName(view, id); throw new IllegalStateException("View '" + name + "' with ID " + id + " for " + who + " was of the wrong type. See cause for more info.", e);} }// ViewBinding 实现Unbinder接口的unBind方法 在对象销毁的时候把对应的ViewBinding类里面的对象进行释放 @Override @CallSuper public void unbind() {//释放ViewBinding中的对象 MainActivity target = this.target; if (target == null) throw new IllegalStateException("Bindings already cleared."); this.target = null; target.tvHellWorld = null; target.etInput = null; if (view7f070024 != null) { view7f070024.setOnClickListener(null); view7f070024 = null; } }
上面的代码都是通过注解获取来的信息再根据JavaPoet按照butterKnife定的规则生成的。
——通过viewBinding构造函数获取到控件、资源和点击事件。赋值给bind()传递过来的绑定对象,实现控件/资源/点击事件的绑定。
——viewBinding类实现Unbinder接口的unbind()方法。
通过源码发现这里释放掉了ViewBinding类的target对象和绑定对象的一些属性与监听。Fragment的实现也基本一样。
那么这里有个问题,为什么在Fragment中需要进行解绑。而在其他对象中不需要解绑?
ButterKnife文档中指出:Fragment具有不同于Activity的生命周期。在onCreateView中绑定一个Fragment时,在onDestroyView中将Fragment设置为null。
可据源码来看解绑显然是把Fragment的一些属性都清空,并且把对应ViewBinding类引用Fragment对象也清空。
如果不进行解绑Fragment ->onDestrory的时候这些就不被回收了吗?
这也是唯一疑惑的一个地方?也许是我对Fragment生命周期仍存在一些不了解地方,很遗憾没有在其他博客和文档中找到我想要的答案。所以在这里提出自己的疑惑。 若有哪位朋友看到,请留言指教一二 。
注:源码就说到这里了,看了别人分析10次源码。不如自己看一次源码的效果好。这是后面自己看源码体会最深的。懂得了原理,使用起来就更得心应手了。
总结:
- 项目编译的时候,ButterKnife使用APT根据注解获取到的信息采用JavaPoet生成ViewBinding类。
- 我们通过ButterKnife.bind()方法传入一个对象与视图
- 通过传入的绑定对象利用反射原理找到对应的viewBinding类并获取到构造函数
- 构造函数通过反射实例化了ViewBinding类并把绑定的对象与视图传到ViewBinding类中
- 在ViewBinding类的构造函数中分别获取到控件与资源再赋值给传递过来的绑定对象(属性与对象绑在一起)
(6)辅助插件
这里推荐一个ButterKnife 辅助插件:Android Butterknife Zelezny (可以自动帮生成所需要绑定的控件)
1.添加插件并重启(这一步就不演示了)
2.右击布局选择Generate...
3.选择需要绑定的控件和点击监听或修改要生成的控件名
六、总结
放弃:
(1)在Goole的大力支持下 ,越来越多人采用kotlin开发,而kotlin有更简便的方式替代了findViewById()方法。使得ButterKnife的作用少了一些,但不是完全没作用。ButterKnife仍可以绑定资源与方法。
(2)DataBinding:使用数据绑定,也可以代替ButterKnife。是不是完全替代就看到家怎么使用了。
或许还有许多我不知道方式。这里就举个例子,也不补充了。
相关链接:
- ButterKnife文档
- ButterKnife GitHb地址
- 反射机制
- APT(Android注解处理器)
- java注解
- 注解视频资源
- JavaPoet
七、内容推荐
《简书》
《Android 10文档阅读总结》
《Android 学习资源收集》
《Android 自定义控件基础》
《Android Rxjava+Retrofit网络请求框架封装(一)》
八、项目参考
自己整理的一个工具演示项目,有兴趣可以看下
Github:https://github.com/DayorNight/BLCS
apk下载体验地址:https://www.pgyer.com/BLCS
若您发现文章中存在错误或不足的地方,希望您能指出!
更多相关文章
- android service 组件
- Android实现多条Toast快速显示(强制中止上一条Toast的显示)
- Android进程管理机制和内存机制
- Dagger2 的简单使用
- Android中内存泄露的原因分析:
- 致Android开发者的Kotlin入门
- Android(安卓)网络请求框架总结(二)
- Android之socket
- Handler ThreadHandler Looper的总结