一、EventBus简介
EventBus是一个Android端优化的
publish/
subscribe消息总线,简化了应用程序内
各组件间、组件与后台
线程间的通信。
作为一个消息总线主要有三个组成部分:
事件(Event):可以是任意类型的
对象。通过事件的发布者将事件进行传递。
事件订阅者(Subscriber):接收特定的事件。
事件发布者(Publisher):用于通知 Subscriber 有事件发生。可以在任意线程任意位置发送事件。
上图解释了整个EventBus的大概工作流程:事件的发布者(Publisher)将事件(Event)通过post()方法发送。EventBus内部进行处理,找到订阅了该事件(Event)的事件订阅者(Subscriber)。然后该事件的订阅者(Subscriber)通过onEvent()方法接收事件进行相关处理(关于onEvent()在EventBus 3.0中有改动,下面详细说明)。
二、EventBus的简单使用
1、把EventBus依赖到项目
build.gradle添加引用
compile 'de.greenrobot:eventbus:3.0.0-beta1'
2、构造事件(Event)对象。也就是发送消息类
每一个消息类,对应一种事件。这里我们定义两个消息发送类。后面讲解具体作用。
public class NewsEvent { private String message; public NewsEvent(String message) { this.message = message; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; }}
public class ToastEvent { private String content; public ToastEvent(String content) { this.content = content; } public String getContent() { return content; } public void setContent(String content) { this.content = content; }}
3、注册/解除事件订阅(Subscriber)
EventBus.getDefault().register(this);//注册事件 其中this代表订阅者
具体注册了对什么事件的订阅,这个需要onEvent()方法来说明。在EventBus 3.0之前,onEvent()方法是用来接收指定事件(Event)类型对象,然后进行相关处理操作。在EventBus 3.0之后,onEvent()方法可以自定义方法名,不过要加入注解@Subscribe。
@Subscribe public void onToastEvent(ToastEvent event){ Toast.makeText(MainActivity.this,event.getContent(),Toast.LENGTH_SHORT).show(); }
通过register(this)来表示该订阅者进行了订阅,通过onToastEvent(ToastEvent event)表示指定对事件ToastEvent的订阅。到这里订阅就完成了。
需要注意的是:一般在onCreate()方法中进行注册订阅。在onDestory()方法中进行解除订阅。
@Override protected void onDestroy() { super.onDestroy(); EventBus.getDefault().unregister(this); }
4 、发送消息
订阅已经完成,那么便可以发送订阅了。
EventBus.getDefault().post(new ToastEvent("Toast,发个提示,祝大家新年快乐!"));
那么onToastEvent(ToastEvent event)会收到事件,并弹出提示。
EventBus的基础使用流程就是这样的。
其实,EventBus还有好多其他的功能。下面我们一个个介绍。
三、EventBus的进阶使用
1.线程模式ThreadMode
当你接收的的事件后,如果处于非UI线程,你要更新UI怎么办?如果处于UI线程,你要进行耗时操作,怎么办?等等其他情况,通过ThreadMode统统帮你解决。
线程间通信
用法展示:
@Subscribe(threadMode = ThreadMode.MainThread) public void onNewsEvent(NewsEvent event){ String message = event.getMessage(); mTv_message.setText(message); }
使用起来很简单,通过
@Subscribe(threadMode = ThreadMode.MainThread)即可指定。
下面具体介绍下ThreadMode。
关于ThreadMode,一共有四种模式PostThread,MainThread,BackgroundThread以及Async。
POSTING
:事件的处理在和事件的发送在相同的线程,所以事件处理时间不应太长,不然影响事件的发送线程。
**MainThread: **事件的处理会在UI线程中执行。事件处理时间不能太长,这个不用说的,长了会ANR的。
BackgroundThread:如果事件是在UI线程中发布出来的,那么事件处理就会在子线程中运行,如果事件本来就是子线程中发布出来的,那么事件处理直接在该子线程中执行。所有待处理事件会被加到一个队列中,由对应线程依次处理这些事件,如果某个事件处理时间太长,会阻塞后面的事件的派发或处理。
Async:事件处理会在
单独的线程中执行,主要用于在后台线程中执行耗时操作,每个事件会开启一个线程。
2.priority事件优先级
事件的优先级类似广播的优先级,优先级越高优先获得消息。
用法展示:
@Subscribe(priority = 100) public void onToastEvent(ToastEvent event){ Toast.makeText(MainActivity.this,event.getContent(),Toast.LENGTH_SHORT).show(); }
当多个订阅者(Subscriber)对同一种事件类型进行订阅时,即对应的事件处理方法中接收的事件类型一致,则
优先级高(priority 设置的值越大),则会先接收事件进行处理;优先级低(priority 设置的值越小),则会后接收事件进行处理。
除此之外,EventBus也可以终止对事件继续传递的功能。
用法展示:
@Subscribe(priority = 100) public void onToastEvent(ToastEvent event){ Toast.makeText(MainActivity.this,event.getContent(),Toast.LENGTH_SHORT).show(); EventBus.getDefault().cancelEventDelivery(event); }
这样其他优先级比100低,并且订阅了该事件的订阅者就会接收不到该事件。
3.EventBus黏性事件
EventBus除了普通事件也支持粘性事件。可以理解成:订阅在发布事件之后,但同样可以收到事件。订阅/解除订阅和普通事件一样,但是处理订阅的方法有所不同,需要注解中添加sticky = true。
用法展示:
@Subscribe(priority = 100,sticky = true) public void onToastEvent(ToastEvent event){ Toast.makeText(MainActivity.this,event.getContent(),Toast.LENGTH_SHORT).show(); EventBus.getDefault().cancelEventDelivery(event); }
这样,假设一个ToastEvent 的事件已经发布,此时还没有注册订阅。当设置了sticky = true,在ToastEvent 的事件发布后,进行注册。依然能够接收到之前发布的事件。
不过这个时候,发布事件的方式就改变了。
EventBus.getDefault().postSticky(new ToastEvent("Toast,发个提示,祝大家新年快乐!"));
我们如果不再需要该粘性事件我们可以移除
EventBus.getDefault().removeStickyEvent(ToastEvent.class);
或者调用移除所有粘性事件
EventBus.getDefault().removeAllStickyEvents();
4.EventBus配置
EventBus在2.3版本中添加了EventBuilder去配置EventBus的各方各面。
比如:如何去构建一个在发布事件时没有订阅者时保持沉默的EventBus。
EventBus eventBus = EventBus.builder().logNoSubscriberMessages(false).sendNoSubscriberEvent(false).build();
通过上述设置,当一个事件没有订阅者时,不会输出log信息,也不会发布一条默认信息。
配置默认的EventBus实例,使用EventBus.getDefault()是一个简单的方法。获取一个单例的EventBus实例。EventBusBuilder也允许使用installDefaultEventBus方法去配置默认的EventBus实例。
注意:不同的EventBus 的对象的数据是不共享的。通过一个EventBus 对象去发布事件,只有通过同一个EventBus 对象订阅事件,才能接收该事件。所以以上使用EventBus.getDefault()获得的都是同一个实例。
下面用一个小栗子来展示下EventBus的应用。
EventBus processor使用:
EventBus提供了一个EventBusAnnotationProcessor注解处理器来在编译期通过读取@Subscribe()注解并解析,
处理其中所包含的信息,然后生成java类来保存所有订阅者关于订阅的信息,这样就比在运行时使用反射来获得这些订阅者的
信息速度要快.
1.)具体使用:在build.gradle中添加如下配置
buildscript { dependencies { classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' }}apply plugin: 'com.neenbedankt.android-apt'dependencies { compile 'org.greenrobot:eventbus:3.0.0' apt 'org.greenrobot:eventbus-annotation-processor:3.0.1'}apt { arguments { eventBusIndex "com.whoislcj.eventbus.MyEventBusIndex" }}
2.)使用索引
此时编译一次,自动生成生成索引类。在\
build\generated\source\apt\PakageName\下看到通过注解分析生成的索引类,这样我们便可以在初始化EventBus时应用我们生成的索引了。
自动生成的代码
/** This class is generated by EventBus, do not edit. */
public
class MyEventBusIndex
implements SubscriberInfoIndex {
private
static
final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;
static { SUBSCRIBER_INDEX =
new HashMap<Class<?>, SubscriberInfo>(); putIndex(
new SimpleSubscriberInfo(com.whoislcj.testhttp.MainActivity.
class,
true,
new SubscriberMethodInfo[] {
new SubscriberMethodInfo("onDataSynEvent", com.whoislcj.testhttp.eventBus.DataSynEvent.
class, ThreadMode.MAIN, 100,
false),
new SubscriberMethodInfo("onDataSynEvent1", com.whoislcj.testhttp.eventBus.TestEvent.
class, ThreadMode.MAIN, 0,
true), })); }
private
static
void putIndex(SubscriberInfo info) { SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info); } @Override
public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) { SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
if (info !=
null) {
return info; }
else {
return
null; } }}
添加索引到EventBus默认的单例中
EventBus.builder().addIndex(
new MyEventBusIndex()).installDefaultEventBus();
3.)对比添加前后注册效率对比
分别EventBus.getDefault().register(this);
添加之前:前后用了
9毫秒
添加之后:前后用了
2毫秒
EventBus优缺点:
优点:简化组件之间的通信方式,实现解耦让业务代码更加简洁,可以动态设置事件处理线程以及优先级
缺点:目前发现唯一的缺点就是类似之前策略模式一样的诟病,每个事件都必须自定义一个事件类,造成事件类太多,无形中加大了维护成本
二、Butterknife
(1)ButterKnife是什么?
在开发过程中,我们总是会写大量的findViewById和点击事件,像初始view、设置view监听这样简单而重复的操作让人觉得特别麻烦,当然不会偷懒的程序员不是好程序员,自然也出现了相应的解决方案--
依赖注入。而ButterKnife则是依赖注入中相对简单易懂的很不错的开源框架,(其实ButterKnife也不算严格意义上的依赖注入,后面文章中会做分析)。但ButterKnife作为JakeWharton大神写的注解框架被广泛应用于android开发中,自然也有它的过人之处。下面对它的使用过程进行描述。
它的具体优势:
1.强大的View绑定和Click事件处理功能,简化代码,提升开发效率
2.方便的处理Adapter里的ViewHolder绑定问题
3.
运行时不会影响APP效率,使用配置方便
4.代码清晰,可读性强
与缓慢的反射相比,Butter Knife使用再编译时生成的代码来执行View的查找,因此不必担心注解的性能问题。调用
bind来生成这些代码,你可以查看或调试这些代码。
例如上面的例子,生成的代码大致如下所示:
public void bind(ExampleActivity activity) { activity.subtitle = (android.widget.TextView) activity.findViewById(2130968578); activity.footer = (android.widget.TextView) activity.findViewById(2130968579); activity.title = (android.widget.TextView) activity.findViewById(2130968577);}
配置ButterKnife
Download
dependencies { compile
'com.jakewharton:butterknife:8.8.1'
annotationProcessor
'com.jakewharton:butterknife-compiler:8.8.1'
}
If you are using Kotlin, replace
annotationProcessor
with
kapt
.
Library projects
To use Butter Knife in a library, add the plugin to your
buildscript
:
buildscript { repositories { mavenCentral() } dependencies { classpath
'com.jakewharton:butterknife-gradle-plugin:8.8.1'
}}
and then apply it in your module:
apply
plugin
:
'com.android.library'
apply
plugin
:
'com.jakewharton.butterknife'
ButterKnife使用中有哪些注意的点呢?
注意:
- Activity ButterKnife.bind(this);必须在setContentView();之后,且父类bind绑定后,子类不需要再bind
- Fragment ButterKnife.bind(this, mRootView);
- 属性布局不能用private or static 修饰,否则会报错
- setContentView()不能通过注解实现。
- ButterKnife已经更新到版本7.0.1了,以前的版本中叫做@InjectView了,而现在改用叫@Bind,更加贴合语义。
- 在Fragment生命周期中,onDestoryView也需要Butterknife.unbind(this)
- ButterKnife不能再你的library module中使用哦!!这是因为你的library中的R字段的id值不是final类型的,但是你自己的应用module中确是final类型的。针对这个问题,有人在Jack的github上issue过这个问题,他本人也做了回答,点击这里。
在非Activity中使用绑定
Butter Knife提供了bind的几个重载,只要传入跟布局,便可以在任何对象中使用注解绑定。
例如在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; }}
还有一种比较常见的场景,就是在ListView的Adapter中,我们常常会使用ViewHolder:
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); } }}
监听器绑定
使用本框架,监听器能够自动的绑定到特定的执行方法上:
@OnClick(R.id.submit)public void submit(View view) { // TODO submit data to server...}
而监听器方法的参数都时可选的:
@OnClick(R.id.submit)public void submit() { // TODO submit data to server...}
指定一个特定的类型,Butter Knife也能识别:
@OnClick(R.id.submit)public void sayHi(Button button) { button.setText("Hello!");}
可以指定多个View ID到一个方法上,这样,这个方法就成为了这些View的共同事件处理。
@OnClick({ R.id.door1, R.id.door2, R.id.door3 })public void pickDoor(DoorView door) { if (door.hasPrizeBehind()) { Toast.makeText(this, "You win!", LENGTH_SHORT).show(); } else { Toast.makeText(this, "Try again", LENGTH_SHORT).show(); }}
自定义View时,绑定事件监听不需要指定ID
public class FancyButton extends Button { @OnClick public void onClick() { // TODO do something! }}
重置绑定:
Fragment的生命周期与Activity不同。在Fragment中,如果你在
onCreateView中使用绑定,那么你需要在
onDestroyView中设置所有view为
null。为此,ButterKnife返回一个
Unbinder实例以便于你进行这项处理。在合适的生命周期回调中调用
unbind函数就可完成重置。
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(); }}
福利
Butter Knife提供了一个
findViewById的简化代码:
findById,用这个方法可以在
View、
Activity和
Dialog中找到想要View,而且,该方法使用的泛型来对返回值进行转换,也就是说,你可以省去
findViewById前面的强制转换了。
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);
如果你只是使用这个方法,可以使用静态引入
ButterKnife.findById方法。
ButterKnife 原理解读
Java Annotation
如果你对注解了解比较少,在进一步了解 ButterKnife 之前,有必要了解一下 Java Annotation 的基础知识。可以参考如下两份资料:
- Mkyong Java Custom Annotations
- Trenia Java Annotations
通过上面的例子以及讲解,我们可以知道如何自定义注解。自定义注解我们需要知道两个基础的元注解
@Retention 和
@Target.
元注解 @Retention
@Retention 保留时间:有三类值可以选择,
SOURCE
RUNTIME
CLASS。
SOURCE 源码时保留:使用此类的注解多为标记注解,比如
@Override、
@Deprecated、
@SuppressWarnings 等。
RUNTIME 运行时保留:程序在运行过程中,使用这些 Annotation, 比如我们常用的
@Test。
CLASS 编译时保留:Java 文件在编译时由 apt 自动解析,需要自定义类继承自 AbstractProcessor 并重写 Process 函数。比如 ButterKnife 中使用的
@BindView,
@OnClick 等就是声明为 CLASS 的。
元注解 @Target
Target 表示注解可以用来修饰哪些元素。可选值包括 TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER 等。
ButterKnifeProcessor
ButterKnife 中所有的注解都使用 Retention 为 CLASS 保留。所以在 ButterKnife 中,有个很重要的 ButterKnifeProcessor。当 java 文件进行编译时,ButterKnifeProcessor 的 process() 方法被调用,生成相关的 ViewBinder 类,用于将 View 或者 Listener 进行绑定。
ButterKnifeProcessor 的关键逻辑如下:
ButterKnifeProcessor.process()
process() 会找到所有 ButterKnife 相关的 annotation,并将这些相应的注解生成为 ViewBinder 类,一个 ViewBinder 示例如下:
Screen Shot 2016-05-11 at 2.52.20 PM.png
ViewBinder 通过 findViewById 实例化 Activity 中页面上的组件,并且能够设置各控件的点击时间,滑动事件等。
为什么 ButterKnife Annotation 的字段或者方法不能声明为 private?
bind() 方法中,通过 activity.homeBtn 对 View 赋值。因此所有 ButterKnife annotation 标记的 field 或者 method 都不能声明为 private, 因为 private 没办法在 ViewBinder 中直接访问。由此可见:
ButterKnife 并不是依靠反射实现 View Injection 的!
ButterKnife.bind(this)
在 ButterKnife 工作之前,我们一定得在设置 View 后调用
ButterKnife.bind(Activity) ,这样才能使 ButterKnife View 注入成功。那么 ButterKnife.bind(Activity) 是如何工作的呢?
ButterKnife.bind(this)
App 在打包时,ButterKnife Processor 就为所有使用 ButterKnife 注解的 Activity 生成 ViewBinder()。通过上述代码可以看到,ButterKnife 会尝试实例化当前 Activity 所关联的所有 ViewBinder 类。实例化之后再调用
ViewBinder.bind(...) 方法,bind() 方法是在 App 运行过程中才会被调用的。于是当前 Avtivity 的 View 被实例化赋值,如果有点击事件的注解,也会绑定相应的事件。
结语
ButterKnife 主要是做 View 注入的,使用起来比较简便。当然如果你想做一些 依赖注入,比如 Android MVP 架构中 Activity 与 Presenter 的依赖。你可能需要借助一些其他的库, 比如 Dagger。研究下 ButterKnife 的原理,才发现这个库做得如此的睿智。