简介
本文通过阅读Objective-C编程一书的第27、28章,对回调的四种方式进行理解。
OC中回调的方式主要有一下四种:
- 目标-动作对(target-action):当某个事件发生时,向指定对象发送特定消息。target指的是对象,action通过消息选择器(selector)选择。
- 辅助对象(helper objects):使用协议的方式,当事件发生时,向遵守响应协议的辅助对象发送消息。(协议类似于Java中的接口类,定义了接口方法,让其他类去实现)。
- 通知(notification):苹果公司提供的方法,通知中心(notification center)对象。程序员向通知中心告知当某个特定事件发生时,向指定的对象发送特定消息。
- Block对象:Block是一段可执行代码,在事件发生时,执行这段代码。Block类似于匿名函数或者lambda,允许程序员将调用的代码和需要回调的代码写到一起,方便阅读。
目标-动作对 (target-action)
OC中的计时器使用的是target-action机制,创建计时器,设定延迟、目标以及动作,在指定的延迟时间后,计时器会向设定的目标发送指定的消息。
接下来的例子,定义一个Logger类,通过计时器调用Logger的方法,打印当前的时间。计时器的使用方式在main方法中。
WHLogger类定义
@interface WHLogger : NSObject
@property (nonatomic) NSDate *lastTime;//记录上一次保存的时间
-(NSString*) lastTimeString;//返回lastTime格式化文本
-(void)updateLastTime:(NSTimer *)t;//更新lastTime,用于计时器的调用
@end
WHLogger类实现
@implementation WHLogger
//定义一个static的NSDateFormatter对象,用于格式化lastTime
//返回lastTime的字符串
-(NSString*) lastTimeString{
static NSDateFormatter *dateFormatter = nil;
if(!dateFormatter){
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setTimeStyle:NSDateFormatterMediumStyle];
[dateFormatter setDateStyle:NSDateFormatterMediumStyle];
NSLog(@"created dateFormatter");
}
return [dateFormatter stringFromDate:self.lastTime];
}
//更新lastTime对象,该函数由计时器周期性调用
-(void)updateLastTime:(NSTimer *)t{
NSDate *now = [NSDate date];
[self setLastTime:now];
NSLog(@"Just set time to %@", self.lastTimeString);
}
@end
mian.m实现
int main(int argc, const char * argv[]) {
@autoreleasepool {
WHLogger *logger = [[WHLogger alloc] init];//logger对象
__unused NSTimer *timer = [NSTimer//创建一个定时器
scheduledTimerWithTimeInterval:2.0//时间间隔2秒,
target:logger//target为logger对象,
selector:@selector(updateLastTime:)//使用@selector语句传递action的消息名称
userInfo:nil
repeats:YES];//重复执行
[[NSRunLoop currentRunLoop] run];//调用系统的运行循环,NSRunLoop会持续等待,并在特定事件触发时,向相应的对象发送消息。
}
return 0;
}
打印结果
2018-06-03 10:47:30.791626+0800 CallbackStudy[44416:5166912] Just set time to 2018年6月3日 上午10:47:30
2018-06-03 10:47:32.788496+0800 CallbackStudy[44416:5166912] Just set time to 2018年6月3日 上午10:47:32
2018-06-03 10:47:34.791369+0800 CallbackStudy[44416:5166912] Just set time to 2018年6月3日 上午10:47:34
2018-06-03 10:47:36.788678+0800 CallbackStudy[44416:5166912] Just set time to 2018年6月3日 上午10:47:36
2018-06-03 10:47:38.790786+0800 CallbackStudy[44416:5166912] Just set time to 2018年6月3日 上午10:47:38
这里有一个小疑问,若在头文件中删除掉updateLastTime方法的声明,仅在.m文件中定义,updateLastTime方法变为一个私有方法,但此时计时器仍能正确调用该方法,系统是如何找到私有方法的?
辅助对象(helper objects)
通过实现协议方法,将对象设置为委托对象,在事件发生时,委托对象的协议方法会被调用。接下来通过网络下载数据的代码来描述委托的使用方式。
在网络中下载数据时,使用同步的方式会导致主线程阻塞,所以在下载东西时,通常使用异步的方式进行下载,通过NSURLConnection的异步模式下载数据。这里在Logger类中实现NSURLConnection的协议,让Logger对象成为NSURLConnection的委托对象(delegate)。
首先看Logger类的定义,该类中声明了要实现的协议
@interface WHLogger : NSObject
<NSURLConnectionDelegate, NSURLConnectionDataDelegate>//声明WHLogger类会实现这两个协议的方法
{
NSMutableData *_incomingData;//保存下载的数据
}
@end
Logger类实现,主要实现了协议中的三个方法
- connection:didReceiveData:收到一定字节数的数据后会被调用
- connectionDidFinishLoading:最后一部分数据处理完毕后调用
- connection:didFailWithError:数据失败时被调用
@implementation WHLogger
//收到一定字节数的数据后会被调用
- (void)connection:(NSURLConnection *)connection
didReceiveData:(NSData *)data
{
NSLog(@"received %lu bytes", [data length]);
//初始化incomingData
if(!_incomingData){
_incomingData = [[NSMutableData alloc] init];
}
[_incomingData appendData:data];
}
//最后一部分数据处理完毕后会被调用
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
NSLog(@"Got it all!");
NSData *data = [[NSData alloc] initWithData:_incomingData];
_incomingData = nil;
NSLog(@"data has %lu characters", [data length]);
[data writeToFile:@"/Users/wilsonhan/Documents/qzx.jpeg" atomically:YES];
}
//获取数据失败时会被调用
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
NSLog(@"connection failed: %@", [error localizedDescription]);
_incomingData = nil;
}
@end
main.m实现
int main(int argc, const char * argv[]) {
@autoreleasepool {
WHLogger *logger = [[WHLogger alloc] init];
NSURL *url = [NSURL URLWithString:@"要下载的数据的网络地址"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
__unused NSURLConnection *fetchConn = [[NSURLConnection alloc]
initWithRequest:request//下载请求
delegate:logger//设置委托对象
startImmediately:YES];
[[NSRunLoop currentRunLoop] run];
}
return 0;
}
通知(Notifications)
这里通过通知中心,获取用户修改Mac系统的时区设置时的NSSystemTimeZoneDidChangeNotification通知。
Logger类的实现
@implementation WHLogger
//用户修改时区时,调用该函数
-(void)zoneChange:(NSNotification *)note{
NSLog(@"The system time zone has changed!");
}
@end
main.m的实现
int main(int argc, const char * argv[]) {
@autoreleasepool {
WHLogger *logger = [[WHLogger alloc] init];
[[NSNotificationCenter defaultCenter]
addObserver:logger//添加logger为观察者
selector:@selector(zoneChange:)//使用选择器传递接收消息的方法
name:NSSystemTimeZoneDidChangeNotification//设置需要接收的通知
object:nil];
[[NSRunLoop currentRunLoop] run];
[[NSNotificationCenter defaultCenter] removeObserver:logger];//要记得移除观察者
}
return 0;
}
代码执行后,logger对象被注册为观察者,当用户修改系统时区时,运行的代码就会打印一条语句,表明接收到了系统通知。
三种回调方式如何做选择
- 对于只做一件事情的对象(如NSTimer),使用target-action
- 对于功能更复杂的对象,使用扶助对象,最常使用的是委托对象
- 对于要触发多个回调的对象,使用通知。
避免强引用循环引用
- 对象不拥有target。应在dealloc方法中将target指针赋为nil
- 对象不拥有委托对象或数据源对象。在dealloc方法中取消相应的关联,调用setDelegate:nil
- 通知中心不拥有观察者,在释放时将对象移出通知中心,在dealloc中调用
[[NSNotificationCenter defaultCenter] removeObserver:self];
Block对象
Block对象是一段代码,没有函数名,由^开始,表明这段代码是一个Block对象。
Block对象类似于其他编程语言的匿名函数、lambda、closure、函数指针等。
这里使用Block实现一个在给定字符串中移除所有元音字母的功能。
main.m的实现
typedef void (^ArrayEnumerationBlock)(id, NSUInteger, BOOL*);
int main(int argc, const char * argv[]) {
@autoreleasepool {
//旧字符串array,保存要处理的所有字符串
NSArray *oldStrings = @[@"Sauerkraut", @"Raygun", @"Big Nerd Ranch", @"Mississippi"];
//打印
NSLog(@"original strings: %@", oldStrings);
//新字符串数组,保存处理后的字符串
NSMutableArray *newStrings = [NSMutableArray array];
//创建数组对象, 保存需要从字符串中移除的字符
NSArray *vowels = [NSArray arrayWithObjects:@"a", @"e", @"i", @"o", @"u", nil];
//声明Block变量
void (^devowelizer)(id, NSUInteger, BOOL *);
//使用typedef的方式声明的Block变量,与上一行代码功能相同
//ArrayEnumerationBlock devowelizer;
//将Block对象赋值给变量
devowelizer = ^(id string, NSUInteger i, BOOL *stop){
//获取传入的字符串
NSMutableString *newString = [NSMutableString stringWithString:string];
//枚举数组中的字符串,将所有出现的元音字母替换成空字符串
for(NSString *s in vowels){
NSRange fullRange = NSMakeRange(0, [newString length]);
[newString
replaceOccurrencesOfString:s
withString:@""
options:NSCaseInsensitiveSearch
range:fullRange];
}
//将处理后的字符串添加到newStrings中
[newStrings addObject:newString];
};
//枚举数组对象,针对每个数组中的对象,执行Block对象devowelizer
[oldStrings enumerateObjectsUsingBlock:devowelizer];
//这里也可以不用声明block对象,直接编写block代码
//[oldStrings enumerateObjectsUsingBlock:^{
// block代码块
//}];
NSLog(@"new strings: %@", newStrings);
}
return 0;
}
打印结果
original strings: (
Sauerkraut,
Raygun,
"Big Nerd Ranch",
Mississippi
)
new strings: (
Srkrt,
Rygn,
"Bg Nrd Rnch",
Msssspp
)
Block对象与其他回调
通过Block对象,可以将回调有关的代码写在与设置回调的代码相同的地方,这样方便其他程序员阅读这段代码。
将通知部分的代码改成Block
在上文通知部分的代码,可以从观察者方式修改成Block的方式,代码如下
[[NSNotificationCenter defaultCenter]
addObserverForName:NSSystemTimeZoneDidChangeNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note){
NSLog(@"The system time zone has changed!");
}];
调用结果与使用addObserver:selector:name:object方法结果相同。
总结
本文通过简单的例子使用了OC中回调的四种方式,了解了如何使用该四种方式,以及不同回调方式适用的场景,接下来需要对不同回调方式的实现原理进行理解。
网友评论