Objective-C 学习笔记 | 回调

参考书:《Objective-C 编程(第2版)》

Objective-C 学习笔记 | 回调

回调就是将一段代码和一个事件绑定起来,当事件发生时,就会执行那段代码。

在 Objective-C 中有 4 种方式来实现回调:

请添加图片描述

本文章将介绍如何通过前三种途径来实现回调,以及怎样根据情况选择合适的途径。

运行循环

NSRunLoop 类专门负责等待事件的发生。NSRunLoop 实例会在特定的事件发生时触发回调。

目标-动作对(target-action)

计时器使用的是目标-动作对机制。创建计时器时,要设定延迟、目标和动作。在指定延迟时间后,计时器会向设定的目标发送指定的消息。

创建一个程序,每隔 2 秒,NSTimer 对象会向其目标(BNRLogger)发送指定的动作消息。如下图所示:

请添加图片描述

main.m:

#import <Foundation/Foundation.h>
#import "BNRLogger.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        BNRLogger *logger = [[BNRLogger alloc] init];
        // __unused 修饰符,消除编译器警告
        __unused NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0
                            target:logger // logger 是 timer 的目标
                            selector:@selector(updateLastTime:) // 传递动作消息的名称
                            userInfo:nil
                            repeats:YES];
        [[NSRunLoop currentRunLoop] run];
    }
    return 0;
}

BNRLogger.h:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface BNRLogger : NSObject

@property (nonatomic) NSDate *lastTime;

- (NSString *)lastTimeString;
// 动作方法
- (void)updateLastTime:(NSTimer *)timer;

@end

NS_ASSUME_NONNULL_END

BNRLogger.m:

#import "BNRLogger.h"

@implementation BNRLogger

- (NSString *)lastTimeString
{
    // static 让所有的 BNRLogger 实例共享一个 NSDateFormatter
    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 *)timer
{
    NSDate *now = [NSDate date];
    [self setLastTime:now];
    NSLog(@"Just set time to %@", self.lastTimeString);
}

@end

运行程序,每隔 2 秒输出当前的日期和时间。

当要向一个对象发送一个回调时,使用目标-动作对。

辅助对象

我们使用一个异步的模式来使用 NSURLConnection,在异步模式下,NSURLConnection 会多次发送块状的数据,BNRLogger 实例会成为 NSURLConnection 的辅助对象,更确切的说,是委托对象。

请添加图片描述

NSURLConnection 有一套协议,协议是一系列方法声明,辅助对象可以根据协议实现这些方法。在下面的程序中,我们声明 BNRLogger 会实现 NSURLConnectionDelegate 和 NSURLConnectionDataDelegate 这两种协议方法,并实现 3 个回调方法。

main.m:

#import <Foundation/Foundation.h>
#import "BNRLogger.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        BNRLogger *logger = [[BNRLogger alloc] init];
        
        NSURL *url = [NSURL URLWithString:@"http://www.gutenberg.org/cache/epub/205/pg205.txt"];
        NSURLRequest *request = [NSURLRequest requestWithURL:url];
        // __unused 修饰符,消除编译器警告
        __unused NSURLConnection *fetchConn =
            [[NSURLConnection alloc] initWithRequest:request
                                        delegate:logger // logger 是 NSURLConnection 的委托对象
                                    startImmediately:YES];
        __unused NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0
                            target:logger // logger 是 timer 的目标
                            selector:@selector(updateLastTime:) // 传递动作消息的名称
                            userInfo:nil
                            repeats:YES];
        [[NSRunLoop currentRunLoop] run];
    }
    return 0;
}

BNRLogger.h:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface BNRLogger : NSObject
    <NSURLConnectionDelegate, NSURLConnectionDataDelegate> // 声明 BNRLogger 会实现这两种协议方法
{
    NSMutableData *_incomingData; // 保存接收的数据
}
@property (nonatomic) NSDate *lastTime;

- (NSString *)lastTimeString;
// 动作方法
- (void)updateLastTime:(NSTimer *)timer;

@end

NS_ASSUME_NONNULL_END

BNRLogger.m:

#import "BNRLogger.h"

@implementation BNRLogger

- (NSString *)lastTimeString
{
    // static 让所有的 BNRLogger 实例共享一个 NSDateFormatter
    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 *)timer
{
    NSDate *now = [NSDate date];
    [self setLastTime:now];
    NSLog(@"Just set time to %@", self.lastTimeString);
}

/** 协议方法 */
// 来自 NSURLConnectionDataDelegate 协议
- (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 *str = [[NSString alloc] initWithData:_incomingData encoding:NSUTF8StringEncoding];
    _incomingData = nil;
    NSLog(@"string has %lu characters", [str length]);
    NSLog(@"The whole string is %@", str);
}

// 来自 NSURLConnectionDelegate 协议
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{ // 获取数据失败时会被调用
    NSLog (@"connection failed %@", [error localizedDescription]);
    _incomingData = nil;
}

@end

运行程序,程序会陆续收到来自 Web 服务器的数据,调用回调方法打印接收到的字节数。最后,当获取数据结束时,委托对象会收到相应的消息,打印接收字符数和完整的数据。

当要向一个对象发送多个回调时,使用符合相应协议的辅助对象。

通知

NSNotificationCenter 类是通知中心,程序中的对象可以通过通知中心将自己注册为观察者。当系统发生变化时,会向通知中心发布特定的通知,然后通知中心会将该通知转发给相应的观察者。

我们将 BNRLogger 实例注册成通知中心的观察者,使之能在系统的时区设置发生变化时能够收到相应的通知,代码如下:

// main.m
				BNRLogger *logger = [[BNRLogger alloc] init];
        
        // 通知
        [[NSNotificationCenter defaultCenter]
            addObserver:logger // 将 logger 注册为观察者
            selector:@selector(zoneChange:)
            name:NSSystemTimeZoneDidChangeNotification // 通知名
            object:nil];

// BNRLogger.m
- (void)zoneChange:(NSNotification *)note
{ // 该方法将在系统发布 NSSystemTimeZoneDidChangeNotification 通知时被调用
    NSLog(@"The system time zone has changed");
}

当要触发多个(其他对象中的)回调的对象时,使用通知。

回调与对象所有权

如果一个对象拥有一个指向回调对象的指针,而回调对象也有指针指向该对象,那么就会陷入强引用循环,这两个对象都无法释放。

请添加图片描述

所以在编写回调相关代码时,应注意以下三点。

第一,通知中心不拥有观察者,释放对象的同时要将其移出通知中心:

- (void)dealloc
{
	[[NSNotificationCenter defaultCenter] removeObserver:self];
}

第二,对象不拥有委托对象或数据源对象。如果一个对象是另一个对象的委托对象或数据源对象,那么释放该对象时应该取消所有的关联:

- (void)dealloc
{
	[windowThatBossesMeAround setDelegate:nil];
  [tableViewThatBegsForData setDataSource:nil];
}

第二,对象不拥有目标。如果一个对象是另一个对象的目标,那么释放该对象时应该将相应的目标指针置空:

- (void)dealloc
{
  [buttonThatKeepsSendingMeMessages setTarget:nil]:
}

深入学习:选择器的工作机制

当某个对象收到消息时,会向该对象的类进行查询,检查是否有与消息名称相匹配的方法。该查询过程会沿着继承层次结构向上,直到某个类回应 “我有与消息名称相匹配的方法”。

请添加图片描述

实际上,为了加快查询速度,编译器会为每个方法附上唯一的数字,查询时按数字而不是方法名,这个数字被称为选择器(selector)。

请添加图片描述

通过编译指令 @selector,可以得到与方法名对应的选择器。

  • 66
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

UestcXiye

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值