27章:回调

回调

本章之前的代码都是“主动的”,这些代码会向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];
}

你可能感兴趣的:(27章:回调)