回调
本章之前的代码都是“主动的”,这些代码会向Foundation对象(NSArray)发送消息,并告诉这些对象应该做什么。本章之前所列举的程序会在运行后立刻退出。
本章我们会一起来创建一个不止是,开始运行,执行,退出的程序。反之,它是一个由事件驱动的程序,这个程序能保持运行,等待事件,并作出相应的处理。除非你关闭程序,否则他不会自动退出,它会一直在后台等待事件的发生。
比如,点击鼠标,触摸事件等。
回调(callback)就是将一段可以执行的代码和一个特定的事情绑定起来,当特定的事件发生时,就会执行这段代码。
目标-动作队(target-action):在程序开始等待前,要求(当指定的事件发生时,向指定的对象发送特定的消息。)这里接受消息的对象是(target),消息的选择器(selector)是动作(action)。
辅助对象(helper objects):在程序开始等待前,要求“当事件发生时,向遵守相应协议的辅助对象发送消息”,委托对象和数据源都是常见的辅助对象。
通知(notification)苹果公司提供了一种称为通知中心(notification)的对象。在程序开始等待前,可以告知通知中心“某个对象正在等待某些特定的通知,当其中某个通知出现时,向指定的对象发送特定的消息。”当事件发生时,相关的对象会向通知中心发布通知,然后再由通知中心将通知转发给正在等待该通知的对象。
Block对象,block是一段可执行的代码,在程序开始等待前,声明一个Block对象,当事件发生时,执行这段Block对象。
运行循环(runloop)
事件驱动的程序需要有一个对象,专门负责等待事件的发生。OSX系统和iOS吸引有一个名为NSRunLoop的类。NSRunLoop实例会持续等待着,当特定的事件发生时,就会向相应的对象发送小徐,NSRunLoop实例会在特定的事件发生时出发回调。
创建一个循环:
int main(int argc, const char * argv[]) {
@autoreleasepool {
[[NSRunLoop currentRunLoop] run];
}
return 0;
}
这时run后,程序不会结束,运行循环正在等待事件的发生。
目标-动作对
计时器使用的是目标-动作对机制。创建计时器时,要设定延迟,目标和动作。在指定延迟时间后,计时器会向设定的目标发送指定的消息
下面要创建一个拥有NSRunLoop对象和NSTimer对象的程序。每隔两秒,NSTimer对象会向其目标发送指定的动作消息。此外,还要创建一个BNRLogger类,这个类的实例将被设置为NSTimer对象的目标,如27.1所示。
@implementation BNRLogger
-(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];
}
-(void)updateLastTime:(NSTimer *)t{
NSDate *now = [NSDate date];
[self setLastTime:now];
NSLog(@"JUST set Time to %@",self.lastTimeString);
}
@end
@interface BNRLogger : NSObject
@property (nonatomic) NSDate *lastTime;
-(NSString *) lastTimeString;
-(void) updateLastTime:(NSTimer *)t;
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
BNRLogger *logger = [[BNRLogger alloc] init];
__unused NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0
target:logger
selector:@selector(updateLastTime:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
return 0;
}
27.3辅助对象
比如 通过NSURLConnection的实例方法从web服务器获取数据。数据的传输时需要同步的,也就是说,所有的数据必须一次传输成功。会有以下两个问题。
1.获取数据是会阻塞主线程。如果在正式的应用中使用该方法,呢么在获取数据时,用户界面会失去响应。
2.某些情况下无法实现回调。例如,当web服务器要求提供用户名和密码是,程序无法通过回调机制来提供相应的信息。
通常会以异步的模式来使用NSURLConnection。在异步模式下,NSURLConnection不会一次发送全部数据,它会发送块状的数据,并多次发送,。也就是说,需要有传输相关的时间,且程序要准备好响应这个事件,相关的事件有:得到数据,web服务器要求提供认证信息或获取数据失败等。
为了实现这种更复杂的传输,我们要使用一个异步的NSURLConnection的辅助对象。更确切的说,BNRLooger对象会成为NSURLConnection的委托对象。
当特定的事件发生时,该对象会向辅助对象发送相应的消息,苹果味NSURLConnection提供了一套协议,协议是一些列方法声明,辅助对象可以更具协议实现相应的方法。
#import "BNRLogger.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
BNRLogger *logger = [[BNRLogger alloc] init];
NSURL *url = [NSURL URLWithString:@"http://ww.gutenberg.org/cache/epub/205/pg205.txt"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
__unused NSURLConnection *fetchConn = [[NSURLConnection alloc] initWithRequest:request
delegate:logger
startImmediately:YES];
__unused NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0
target:logger
selector:@selector(updateLastTime:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
return 0;
}
#import
@interface BNRLogger : NSObject
{
NSMutableArray *_incomingData;
}
@property (nonatomic) NSDate *lastTime;
-(NSString *) lastTimeString;
-(void) updateLastTime:(NSTimer *)t;
@end
//
// BNRLogger.m
// CallBack
//
// Created by 啦啦哥 on 2018/5/31.
// Copyright © 2018年 啦啦哥. All rights reserved.
//
#import "BNRLogger.h"
@implementation BNRLogger
-(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];
}
-(void)updateLastTime:(NSTimer *)t{
NSDate *now = [NSDate date];
[self setLastTime:now];
NSLog(@"JUST set Time to %@",self.lastTimeString);
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
NSLog(@"received %lu Bytes",[data length]);
if(!_incomingData){
_incomingData = [[NSMutableArray alloc] init];
}
[_incomingData addObject:data];
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
NSLog(@"Got it all");
NSString *string = [[NSString alloc] initWithData:_incomingData encoding:NSUTF8StringEncoding];
_incomingData = nil;
NSLog(@"String has %lu characters",[string length]);
}
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error{
NSLog(@"connection failed :%@",[error localizedDescription]);
_incomingData = nil;
}
@end
目前的回调规则如下,当要向一个对象发送一个回调时,用目标-动作对(target-action)。当要向一个对象发送多个回调时,可以使用符合相应协议的辅助对象。
27.4通知
当用户修改Mac系统的时区设置时,程序中的很多对象可能需要知道系统发生的这一变化,这些对象都可以通过通知中心,将自己注册成为观察者。
当系统的时区设置发送变化时,会向通知中心发布NSSystemTimeZoneDidChangeNotification通知,然后通知中心会将该通知转发给相应的观察者。
在main.h中,将BNRLogger实例注册为观察者,使之在系统的时区设置发生变化时能够受收到相应的通知,代码如下:
#import
#import "BNRLogger.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
BNRLogger *logger = [[BNRLogger alloc] init];
[[NSNotificationCenter defaultCenter]addObserver:logger
selector:@selector(zoneChange:)
name:NSSystemTimeZoneDidChangeNotification
object:nil];
NSURL *url = [NSURL URLWithString:@"http://ww.gutenberg.org/cache/epub/205/pg205.txt"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
__unused NSURLConnection *fetchConn = [[NSURLConnection alloc] initWithRequest:request
delegate:logger
startImmediately:YES];
__unused NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0
target:logger
selector:@selector(updateLastTime:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
return 0;
}
#import "BNRLogger.h"
@implementation BNRLogger
//···········
//··········
-(void)zoneChange:(NSNotification *)note{
NSLog(@"the system time zone has changed!");
}
@end
向通知中心注册观察者时,你可以知道某个特定的通知名(例如:NSWindowDidResizeNotification)以及发布通知的来源(比如“我只想接受到这个窗口调整的大小的通知”)。这两个参数你都可以设置为nil。但是,如果你将这两个参数都设置为nil,就会接收到程序中所有对象发布的每条通知,对一个桌面应用来说,通知的数量非常多
27.5如何选择
对于只做一件事情的对象(例如NSTimer),使用目标-动作对。
对于功能更复杂的对象(例如NSURLConnection),使用辅助对象,最常见的辅助对象类型时委托对象
对于要触发多个(其他对象中的)回调的对象(例如NSTimeZone),使用通知
27.6回调与对象所有权
无论是哪种类型的回调,如果代码编写不正确,那么都有陷入强引用循环的风险。常发生这种情况:创建的对象拥有一个指向回调对象的指针。而这个回调对象的指针指向你创建的对象,他们彼此之间具有强引用关系,最后陷入一个强引用循环,这两个对象都无法释放。
所以在编写代码时,应该遵循以下规则。
通知中心不拥有观察者。如果将某个对象注册为观察者,那么通常应该在释放该对象时将其移出通知中心。
-(void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObsever:self];
}
对象不拥有委托对象或数据源对象。如果某个新创建的对象是另一个对象的委托对象或数据源对象。那么该对象应该在其dealloc方法中取消相应的关联。
-(void)dealloc
{
[windowThatBossesMeAround setDelegate:nil];
[tableViewThatBegsForData setDataSource:nil];
}
对象不拥有目标。如果某个新创建的对象是另一个对象的目标,那么该对象应该在其dealloc方法中将相应目标指针覆为nil
-(void)dealloc
{
[buttonThatKeepsSendingMeMessages setTarget : nil];
}