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,可以得到与方法名对应的选择器。