使用Objective-C通常有三种方式实现回调:
Target-action:当某事件发生时,向指定的对象发送某个特定的消息。只做一件事时使用。
Helper objects:当某事件发生时,向遵循相应协议的辅助对象发送消息。代理和数据源是常见的辅助对象。功能较复杂是使用。
Notification:某个对象正在等待某些特定的通告,当其中的某个通告出现时,向指定的对象发送特定的消息。当某事件发生时,相关的对象会向通告中心发布通告,然后再有通告中心将通告转发给正在等待该通告的对象。要触发多个回调对象时使用。
Target-action
NSTimer使用就是这个机制。创建该对象时,要设定延时,目标和动作。在指定的延时时间后,该对象会向设定的目标发送指定的消息。
新建工程Callbacks,添加类Logger
#import <Foundation/Foundation.h>
@interface Logger : NSObject
- (void)sayOuch:(NSTimer *)t;
@end
#import "Logger.h"
@implementation Logger
- (void)sayOuch:(NSTimer *)t
{
NSLog(@"Ouch!");
}
@end
创建logger实例和NSTimer实例,指定当timer超时是,向logger发送sayOuch:消息,这里需要用到 @selector。
#import <Foundation/Foundation.h>
#include "Logger.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Logger *logger = [[Logger alloc] init];
__unused NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:logger selector:@selector(sayOuch:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
return 0;
}
程序执行效果如下,定时器每2秒超时一次,调用logger的sayOuch方法,打印"Ouch!"
2015-01-30 21:45:13.905 Callbacks[4612:55160] Ouch!
2015-01-30 21:45:15.902 Callbacks[4612:55160] Ouch!
2015-01-30 21:45:17.900 Callbacks[4612:55160] Ouch!
2015-01-30 21:45:19.901 Callbacks[4612:55160] Ouch!
Helper objects
NSTimer比较简单,完成了一项任务:在指定的时刻触发时间。但是类似通过NSURLConnection的sendSynchronousRequest:returningResponse:error:方法从Web服务器获取数据这样的操作就会有两个问题:
1. 获取数据时会阻塞主线程
2. 某些情况下无法实现回调,例如,Web服务器要求用户提供用户名和密码时。
鉴于以上的原因,通常会以异步的模式来使用NSURLConnection。程序会先要求NSURLConnection获取数据,然后等待回调。回调会在以下时间发生时触发:得到数据、Web服务器要求提供认证信息或获取数据失败等。
NSURL *url = [NSURL URLWithString:@"http://www.baidu.com/img/bdlogo.png"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
__unused NSURLConnection *fetchConn = [[NSURLConnection alloc] initWithRequest:request delegate:logger startImmediately:YES];
修改Logger类,使用NSMutableData来保存服务器返回的数据
#import <Foundation/Foundation.h>
@interface Logger : NSObject
{
NSMutableData *incomingData;
}
- (void)sayOuch:(NSTimer *)t;
@end
实现回调函数,接收数据,完成数据接收和接收数据失败:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSLog(@"received %lu bytes", [data length]);
if (!incomingData)
{
incomingData = [[NSMutableData alloc] init];
}
[incomingData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSLog(@"Got it all!");
NSString *string = [[NSString alloc] initWithData:incomingData encoding:NSUTF8StringEncoding];
NSLog(@"string has %lu characters", [string length]);
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
NSLog(@"connection failed: %@", [error localizedDescription]);
incomingData = nil;
}
程序执行结果如下:
received 5331 bytes
incomingData has 5331 bytes
Got it all!
Notification
当用户修改Mac系统的时区设置时,程序中的很多对象可能需要知道系统发生了这一变化。这些对象都可以通过通知中心将自己注册为观察者。
[[NSNotificationCenter defaultCenter] addObserver:logger selector:@selector(zoneChange:) name:NSSystemTimeZoneDidChangeNotification object:nil];
在logger类中添加回调方法
- (void)zoneChange:(NSNotification *)note
{
NSLog(@"the system time zone has changed!");
}
回调与对象所有权
无论哪种类型的回调,如果代码编写不正确,都可能使得等待回调的对象得不到正确的释放。编码时应遵守以下规则:
1. 通告中心不拥有其下的观察者,在释放对象时将其移出通告中心
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
2. 对象不拥有其下的委托对象或数据源对象,如果某对象是另一个对象的委托对象或数据源对象,那么该对象应该在dealloc方法中取消相应的关联
- (void)dealloc
{
[windowThatBossesMeAround setDelegate:nil];
[tableViewThatBegsForData setDataSource:nil];
}
3. 对象不拥有其目标,如果某个新创建的对象是另一个对象的目标,那么该对象应该在其dealloc方法中将相应的目标指针赋值为nil
- (void)dealloc
{
[buttonThatKeepsSendingMeMessages setTarget:nil];
}
Protocols
iOS应用程序经常会使用UITableView实例来显示数据,但是UITableView对象自身并不包含要显示的数据,必须从其它对象处获取。因此,程序必须告诉UITableView对象,某个类是它的数据源角色。使用协议可以指定某个对象时UITableView的数据源。这个类需要实现相应的必须方法(required method)并选择性的实现部分可选方法(optional method)。
UITableView的数据源协议是UITableViewDataSource。
@interface TerrificViewController : UIViewController <UITableViewDataSource>
...
@end
这段代码意思是:TerrificViewController是UIViewController的子类,并遵守UITableViewDataSource协议。
如果某个类要遵守多个协议,则都写在尖括号内并以逗号分隔。
@interface TerrificViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate>
...
@end