你的位置:首页 > 操作系统

[操作系统]Android 中 EventBus 的使用(2):缓存事件


在上一篇文章中,我曾提到我所选择的是Green Robot提供的EventBus(Android平台),而且这并非只是我一个人的选择。在最近一次查看中,我发现选择它的人数已经是Otto(由Jake Wharton和其他大神们在Square上所提供的版本)的两倍之多了。GR的版本显然比Otto有更多的性能提升,但最令我动心的地方在于它添加了很多新功能。今天我就打算谈谈其中的一项:通过sticky事件进行事件缓存。

sticky是什么?

sticky事件就是指在EventBus内部被缓存的那些事件。EventBus为每个类(class)类型保存了最近一次被发送的事件——sticky。后续被发送过来的相同类型的sticky事件会自动替换之前缓存的事件。当一个监听者向EventBus进行注册时,它可能会去请求这些缓存事件。这时,所有已缓存的事件就会立即自动发送给这个监听者,就象这些事件又重新刚被发送了一次一样。这就意味着,一个监听者可以收到在它注册之前就已经被发送到EventBus中的事件(甚至是在这个监听者的实例被创建出来前,这一点是不是很奇妙)。这一强大功能将有助于我们解决某些固有的问题,如Android上跨Activity和Fragment生命周期传递数据这种复杂问题,异步调用等等。

使用sticky
使用sticky事件需要从两个方面进行:

  • 一、发送者必须通过调用bus.postSticky(event)将事件进行缓存。
  • 二、监听者须调用bus.registerSticky(this)以获取缓存的事件。

当调用了bus.registerSticky(this)后,监听者会立即收到所有已在onEvent处理程序中定义过的那些已缓存的事件。另外,监听者也可以根据需要通过bus.getStickyEvent(SomeEvent.class)来获取这些缓存事件。

(注:调用postSticky,会像普通的post调用一样将事件发送给所有当前活动的监听者,而不是仅限于那些通过registerSticky注册的。registerSticky仅仅是使缓存事件在注册时被重发。)

sticky事件在缓存中存在的时间并不确定。所以如果你想在某一时刻消除缓存中的事件好让它们不再被发送,可以通过bus.removeStickyEvent(event)或bus.removeStickyEvent(SomeEvent.class),以及bus.removeAllStickyEvents()来实现。

令人厌恶的Bundles

我在上一篇文章中曾说过,我并不喜欢Android中的Bundle,而且尽量避免使用它们。我不喜欢被象Serializable或者是Parcelable这类对象所约束,尤其是它们还缺少对类型安全的检查。我的意思是必竟这是Java,而不是Python或者Javascript什么的。我希望我的IDE能够发现并告诉我这样的错误,如一个组件向另外一个组件发送了一个不是它期望的对象类型。

不要误会,Intent在进程间通信时还是很有用的,在这种情况下将携带的数据序列化成通用格式是合情合理的。但如果仅仅是为了在用户旋转了一下屏幕后,让程序保持原来的状态,以科学的名义说,有必要非得用这种方法么?没错,我说的就是Android提供的处理配置改变的标准模式——在onSaveInstanceState(Bundle bundle)和onRestoreInstanceState(Bundle bundle)中保存和恢复状态数据。且不提那些荒唐复杂的Fragment生命周期问题,单单是保持运行状态的这种处理方式就是我最不喜欢的Android开发特点之一。

Stinky Bundles Sticky Events

程序运行状态除了保存到Bundle中,另一种方法是将它们保存到某些在配置改变时依然生存着的对象中去。GR的EventBus刚好内置了这种缓存机制可供我们使用。

考虑下面这个响应“Master/Detail流程”的标准场景:

  • 有一个List组件(通常为Fragment)显示一个摘要列表。
  • 另外一个组件(另一个Fragment)显示每一项的详细内容。
  • 点击某一列表项可显示对应的详细信息。
  • 在竖屏模式中,列表和详细信息分为两页,各占一屏,每次只能看到一个页面。
  • 在横屏模式中,列表在屏幕的左侧,详细信息在右侧,当左侧的列表项被选中时,右侧的详细信息也随之改变。
  • 主Activity中包含一个布局(layout)用于在不同模式下进行切换。

在这个例子中具有挑战性的是,当用户在横竖屏间来回切换时,程序要如何维护当前所选项的状态。这个状态的重要性不但在于详细页面需要知道该显示哪条详细信息,而且列表也需要显示出当前哪一项被选中了。此外,对于主页面来说也需要知道当前是否有项目被选中,以便决定在竖屏模式时需要加载哪个页面,列表或详细信息。

如你所见,这三个组件都需要同一个状态信息(被选中项)。使用传统方法,这三个组件每一个都需要在各自的onSaveInstanceState方法中将这一状态保存进Bundle中,然后再从各自的onResumeInstanceState方法里把数据取回来。不爽!

然而使用sticky事件,事情就变得简单多了。为了更好地说明问题,我创建了一个Android示例工程:https://github.com/wongcain/EventBus-Config-Demo/ 下面所有的示例代码都包含在这个工程里。

首先,创建一个事件类(ItemSelectedEvent.java)用于传递被选中项的位置信息:

 
 
 
 
 
Java

 
1
2
3
4
5
6

public class ItemSelectedEvent {
public final int position;
public ItemSelectedEvent(int position) {
this.position = position;
}
}



然后在List组件(ItemListFragment.java)的listItemClick方法里发送一个sticky事件:

 
 
 
 
 
 
Java

 
1
2
3
4
5

@Override
public void onListItemClick(ListView listView, View itemView, int position, long id) {
super.onListItemClick(listView, itemView, position, id);
bus.postSticky(new ItemSelectedEvent(position));
}



接下来,Detail组件(ItemDetailFragment.java)注册接收sticky事件,并定义一个ItemSelectedEvent的处理方法。当收到事件时,查询并显示被选中项的详细信息:

 
 
 
 
 
Java

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

@Override
public void onResume() {
super.onResume();
bus.registerSticky(this);
}
 
@Override
public void onPause() {
bus.unregister(this);
super.onPause();
}
 
...
 
public void onEvent(ItemSelectedEvent event) {
Item item = MockDataSource.ITEMS.get(event.position);
titleView.setText(item.title);
dateView.setText(item.getDateStr());
bodyView.setText(item.body);
}



最后,在Main组件(MainActivity.java)中将所有内容集合到一起。Activity自身注册监听sticky事件,并创建与Detail组件一样的ItemSelectedEvent处理方法。当收到事件时,根据当前页面布局(layout)决定将Detail fragment加载哪个合适的容器中。

 
 
 
 
 
Java

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

@Override
protected void onResume() {
super.onResume();
bus.registerSticky(this);
bus.postSticky(new LayoutEvent(isTwoPane()));
}
 
@Override
protected void onPause() {
bus.unregister(this);
super.onPause();
}
 
public void onEvent(ItemSelectedEvent event) {
if(isTwoPane()){
getFragmentManager().beginTransaction()
.replace(detailContainer.getId(), new ItemDetailFragment())
.commit();
} else {
getFragmentManager().beginTransaction()
.replace(listContainer.getId(), new ItemDetailFragment())
.addToBackStack(ItemDetailFragment.class.getName())
.commit();
}
}



注意,这个activity不仅监听sticky事件,还发送了另外一个sticky事件用来传递当前屏幕模式。这一事件随后会被List fragment(ItemListFragment.java)收到,并且根据条件对列表进行设置:

 
 
 
 
 
Java

 
1
2
3
4
5
6
7
8

public void onEvent(LayoutEvent event) {
if(event.isTwoPane){
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
updateSelectedItem(activePosition);
} else {
getListView().setChoiceMode(ListView.CHOICE_MODE_NONE);
}
}



另外可以看到,没有一个组件要去实现onSaveInstanceState(Bundle bundle)以及onRestoreInstanceState(Bundle bundle)方法。取而代之的是它们只需简单地依赖于在registerSticky(this)时自动发送的缓存事件。所以,当用户选择一个项目并且在查看详细信息时,以下情况便会在配置改变时自动发生:

  1. 在onPause时,每个组件都会将自身从EventBus注销掉。
  2. Main activity重启并在它的onResume方法里注册监听sticky事件。
  3. 缓存的ItemSelectedEvent被发送到Main activity,然后Detail fragment被加载。
  4. Detail fragment的onResume被调用并且接收到ItemSelectedEvent,从而使得被选中项目的详细信息被显示出来。
  5. 此外,List fragment的onResume被调用并且收到ItemSelectedEvent和LayoutEvent,然后根据当前布局正确地显示被选中项目。

希望这篇文章对你能有帮助。如之前提到的,所有的示例代码都可以在这里访问到:https://github.com/wongcain/EventBus-Config-Demo/

下一篇将是有关EventBus系列教程的最后一篇, 我将谈一谈在EventBus中有关跨越多线程和进程的有关内容。

全能程序员交流QQ群290551701,聚集很多互联网精英,技术总监,架构师,项目经理!开源技术研究,欢迎业内人士,大牛及新手有志于从事IT行业人员进入!