观察者(Observer)模式
在观察者模式中,一个对象任何状态的变更都会通知另外的对改变感兴趣的对象。这些对象之间不需要知道彼此的存在,这其实是一种松耦合的设计。当某个属性变化的时候,我们通常使用这个模式去通知其它对象。
此模式的通用实现中,观察者注册自己感兴趣的其它对象的状态变更事件。当状态发生变化的时候,所有的观察者都会得到通知。苹果的推送通知(
Push Notification
)就是一个此模式的例子。
如果你要遵从
MVC
模式的概念,你需要让模型对象和视图对象在不相互直接引用的情况下通信。这正是观察者模式的用武之地。
Cocoa
通过通知(
Notifications
)和
Key-Value Observing(KVO)
来实现观察者模式。
通知(Notifications)
不要和远程推送以及本地通知所混淆,通知是一种基于订阅
-
发布模式的模型,它让发布者可以给订阅者发送消息,并且发布者不需要对订阅者有任何的了解。
通知在苹果官方被大量的使用。举例来说,当键盘弹出或者隐藏的时候,系统会独立发送
UIKeyboardWillShowNotification
/
UIKeyboardWillHideNotification
通知。当你的应用进入后台运行的时候,系统会发送一个
UIApplicationDidEnterBackgroundNotification
通知。
注意
:
打开
UIApplication.h,
在文件的末尾,你将看到一个由系统发出的超过
20
个通知组成的列表。
如何使用通知(Notifications)
打开
AlbumView.m
,在
initWithFrame:albumCover::
方法的
[self addSubview:indicator];
语句之后加入如下代码:
[[NSNotificationCenter defaultCenter] postNotificationName:@"BLDownloadImageNotification"
object:self
userInfo:@{@"imageView":coverImage, @"coverUrl":albumCover}];
这行代码通过
NSNotificationCenter
单例发送了一个通知。这个通知包含了
UIImageView
和需要下载的封面
URL
,这些是你下载任务所需要的所有信息。
在
LibraryAPI.m
文件
init
方法的
isOnline=NO
之后,增加如下的代码:
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadImage:) name:@"BLDownloadImageNotification" object:nil];
这个是观察者模式中两部分的另外一部分:观察者。每次
AlbumView
发送一个
BLDownloadImageNotification
通知,因为
LibraryAPI
已经注册为同样的通知的观察者,那么系统就会通知
LibraryAPI,LibraryAPI
又会调用
downloadImage:
来响应。
然而在你实现
downloadImage:
方法之前,你必须在你的对象销毁的时候,退订所有之前订阅的通知。如果你不能正确的退订的话,一个通知发送给一个已经销毁的对象会导致你的
app
崩溃。
在
Library.m
中增加下面的代码
:
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
当对象被销毁的时候,它将移除所有监听通知的观察者。
还有一件事情需要去做,将已经下载的封面图片本地存储起来是个不错的主意,这样可以避免每次都重新下载相同的封面。
打开
PersistencyManager.h
文件,增加下面两个方法原型:
- (void)saveImage:(UIImage*)image filename:(NSString*)filename;
- (UIImage*)getImage:(NSString*)filename;
在
PersistencyManager.m
文件中,增加方法的实现:
- (void)saveImage:(UIImage*)image filename:(NSString*)filename
{
filename = [NSHomeDirectory() stringByAppendingFormat:@"/Documents/%@", filename];
NSData *data = UIImagePNGRepresentation(image);
[data writeToFile:filename atomically:YES];
}
- (UIImage*)getImage:(NSString*)filename
{
filename = [NSHomeDirectory() stringByAppendingFormat:@"/Documents/%@", filename];
NSData *data = [NSDatadataWithContentsOfFile:filename];
return [UIImage imageWithData:data];
}
上面的代码相当直接。下载的图片会被保存在文档(
Documents
)目录,如果在文档目录不存在指定的文件,
getImage:
方法将返回
nil.
现在在
LibraryAPI.m
中增加下面的方法:
- (void)downloadImage:(NSNotification*)notification
{
// 1
UIImageView *imageView = notification.userInfo[@"imageView"];
NSString *coverUrl = notification.userInfo[@"coverUrl"];
// 2
imageView.image = [persistencyManager getImage:[coverUrl lastPathComponent]];
if (imageView.image == nil)
{
// 3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *image = [httpClient downloadImage:coverUrl];
// 4
dispatch_sync(dispatch_get_main_queue(), ^{
imageView.image = image;
[persistencyManager saveImage:image filename:[coverUrl lastPathComponent]];
});
});
}
}
下面是以上代码分段描述:
1.
downloadImage
方法是通过通知被执行的,所以通知对象会当作参数传递。
UIImageView
和图片
URL
都会从通知中获取。
2.
如果图片已经被下载过了,直接从
PersistencyManager
方法获取。
3.
如果图片还没有被下载,通过
HTTPClient
去获取它。
4.
当图片下载的时候,将它显示在
UIImageView
中,同时使用
PersistencyManager
保存到本地。
再一次,你使用了门面(
Facade
)模式隐藏了下载图片的复杂性。通知的发送者不需要关心图片是来自网络还是来自本地文件系统。
构建并运行你的应用,看看那些在滚动视图中的漂亮封面吧:
停止你的应用再一次运行它,你会注意到不会存在加载图片的延迟,因为它们都已经被保存到了本地。甚至你可以断开网络,你的应用也可以完美地运行。然而这里有点奇怪,图片上的提示转盘一直在转动,出了什么问题呢?
当开始下载图片的时候,你启动了提示图片正在加载的旋转提示器,但是你还没有实现图片下载完成后停止它的逻辑。你应该在每次图片下载完成的时候发送一个通知,但是这里你使用
KVO
这种观察者模式。
Key-Value Observing(KVO)模式
在
KVO
中,一个对象可以要求在它自身或者其它对象的属性发送变化的时候得到通知。如果你对
KVO
感兴趣的话,你可以更进一步的阅读这篇文章:
Apple’s KVO Programming Guide
.
如何使用KVO
正如上面所说的,
KVO
机制让对象可以感知到属性的变化。在本例中,你可以使用
KVO
去观察
UIImageView
的
image
属性的变化。
打开
AlbumView.m
文件,在
initWithFrame:albumCover:
方法
[self addSubview:indicator]
这一行后,增加下面的代码:
[coverImage addObserver:self forKeyPath:@"image" options:0 context:nil];
这里它增加了它自己(当前的类)作为
image
属性的观察者。
当完成的时候,你同样需要注销相应的观察者。仍然在
AlbumView.m
中增加下面的代码:
- (void)dealloc
{
[coverImage removeObserver:self forKeyPath:@"image"];
}
最后增加下面的方法:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"image"])
{
[indicator stopAnimating];
}
}
你必须在每个观察者类中实现这个方法。系统会在被观察的属性发送变化的时候通知观察者。在上面的代码中,当
image
属性变化的时候,你停止了封面上面的旋转提示器。这样以来,当图片加载完后,旋转提示器将会停止。
构建并运行的你的工程。旋转提示器应该会消失:
注意
:
你要总是记得去移除已经销毁的观察者,否则当给不存在的观察者发送消息的时候,你的应用可能会崩溃。
如果你玩一回你的应用后终止它,你会发现你的应用状态没有被保存,你上次查看的专辑不是下次启动时候的缺省专辑。
为了修正这个问题,你可以使用列表中的下个模式:备忘录(
Memento
)模式
.
原文出处:http://xmuzyq.iteye.com/blog/1942381