iOS回调方法总结(超详细总结)

声明:未经许可,禁止转载。

整个项目的Gihub地址:https://github.com/LeeLom/CallBackDemo


回调(callback)就是将一段可执行的代码和一个特定的事件绑定起来,当特定的时间发生时,就会执行这段代码。
在Objective-C中,有四种途径可以试下回调:

iOS回调方式.png
  • 目标-动作对(Targe-Action): 在程序开始等待前,要求“当事件发生时,向指定的对象发送某个特定的消息”。这里接受消息的对象是目标(target),消息的选择器(selector)是动作(action).
  • 辅助对象(Helper Objects): 在程序开始等待前,要求“当事件发生时,向遵守相应协议的辅助对象发送消息”。Delegate 和 DataSource是我们常见的辅助对象
  • 通知(Notification): 某个对象正在等待某些特定的通知。当其中的某个通知出现时,向指定的对象发送特定的消息。当事件发生时,相关的对象会向通知中心发布通知,然后再有通知中心将通知转发给正在等待该通知的对象
  • Blocks: 在程序开始等待前,声明一个Block对象,当事件发生时,执行这段Block对象。

在iOS开发中最常使用的就是辅助对象和Blocks. 下面将会通过四个例子来看一下这四种回调方式都是怎么实现的。

目标-动作对 (Target-Action)


  • 创建一个NSRunLoop对象和NSTimer对象的程序。

这个程序每隔2秒,NSTimer就会像其目标发送指定的动作消息。此外,在创建一个Logger类,这个类的实例将被设置为NSTimer对象的目标。

//1. 目标-动作对
// 创建一个Logger的实例logger
Logger *logger = [[Logger alloc]init];
// 每隔2秒,NSTimer对象会向其Target对象logger,发送指定的消息updateLastTime:
__unused NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0
                                                  target:logger
                                                selector:@selector(updateLastTime:)
                                                userInfo:nil
                                                 repeats:YES];

输出结果:

WX20170913-105013@2x.png

Target: logger
Action: logger对象的updateLastTime方法

  • 在程序中常见的按钮点击事件也是这种类型。首先,我们用代码创建了一个按钮btn,然后为这个按钮添加他的目标为当前的AppDelegate(这里仅仅是为了举例,一般我们都是用在ViewController当中),对应的Action为:btnClick
// 创建一个按钮
UIButton *btn = [[UIButton alloc]init];
// 为按钮添加事件
[btn addTarget:self
        action:@selector(btnClick)
forControlEvents:UIControlEventTouchUpInside];
- (void)btnClick {
    NSLog(@"按钮点击事件");
}

从这种目标-动作的回调方式我们可以发现,NSTimer它只负责一件事情updateLastTime,btn它只负责btnClick。也就是说,对于只做一件事情的对象,我们可以是使用目标动作对。

辅助对象 (Delegate/Datasource)


  • 辅助对象是在iOS开发中相当常见的。比如我们经常使用的UITableView这个空间,相信大家都使用过其中的UITableViewDelegate以及UITableViewDataSource
self.tableView.delegate = self;
self.tableView.dataSource = self;

上面的两行代码,我们在某个ViewController当中使用的话,意味着我们将ViewController设置成为了tableView的辅助对象。当tableView需要更新或者是响应某些特定的事件时,就会向该ViewController发送消息。
具体发送哪些消息就看我们怎么实现的了,比如我们点击某行需要响应点击事件时,我们就需要实现下面这个方法:

// Called after the user changes the selection.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;

关于UITableView的使用网上有大量的资料,在这里就不再重复了。我们在这个部分只要明确一点,UITableView它的回调方式是通过这种委托对象来实现的,而委托的对象通常是使用它的ViewController,我们需要委托对象为UITableView完成什么事情,就需要在委托对象ViewController中实现相应的协议Protocol(也即delegatedatasource)。

  • 下面,我们通过一个网络异步下载的例子,进一步加深了解这种辅助对象的回调。

我们使用NSURLConnection从服务器获取数据时,通常都是通过异步方式完成的,NSURLConnection通常不会一次就发送全部数据,而是多次的发送块状数据。也就是说,我们需要在程序中不断的响应接受数据的事件。

因此,我们需要一个对象来帮助NSURLConnection完成这些操作。继续前面的例子,我们使用Logger类的实例来完成。因为要完成NSURLConnection的操作,所以Logger当中要实现它的协议,在这个简单的例子中,我们只需要实现NSURLConnection的三个协议方法就好。

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;

PS: 其中1.2是NSURLConnectionDataDelegate, 第三条是NSURLConnectionDelegate.

//2. 辅助对象
NSURL *url = [NSURL URLWithString:@"https://www.gutenberg.org/cache/epub/205/pg205.txt"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
__unused NSURLConnection *fetchConn = [[NSURLConnection alloc]initWithRequest:request
                                                                     delegate:logger
                                                             startImmediately:YES];

在这里,我们将logger设置为了NSURLConnection的辅助对象,因此网络下载相关的信息都会在辅助对象logger中进行响应。
输出的结果如下:

WX20170913-113155@2x.png

从上面的UITableViewNSURLConnection的例子中我们可以发现,辅助对象和目标动作对的实现逻辑非常相似,如果吧目标理解为辅助对象,动作理解为协议的话,二者几乎是一一对应的。但是二者的区别主要在于:当要向一个对象发送多个回调的时候,通常选择符合相应协议的辅助对象;如果要向一个对象发送一个回调是,通常使用目标动作对。

辅助对象也常被成为委托对象delegate和数据源datasource

通知 Notifications


上面所说的目标动作对和辅助对象都是向一个对象发送消息,如果要向多个对象发送消息,那么我们就需要使用通知这种方式了。

  • 例子1: 我们使用电脑的时候发现,当改变系统的失去设置时,程序中的很多对象都可以知道这一变化。之所以能够实现,是因为这些对象都可以通过通知中心将自己注册成为观察者Observer。当系统是时区发生改变的时候,会像通知中心发布NSSystemTimeZondeDidChangeNotification通知,然后通知中心将该通知转发给所有注册了该Name的观察者。

同样的,我们继续在Logger这个类中继续进行操作。这次,我们Logger的实例注册为观察者,让它能够在系统的失去发生变化的时候收到相应的通知。

[[NSNotificationCenter defaultCenter]addObserver:logger
                                       selector:@selector(zoneChange:)
                                           name:NSSystemTimeZoneDidChangeNotification
                                         object:nil];
return YES;
- (void)zoneChange:(NSNotification *)note {
   NSLog(@"The system time zone has changed!");
}

(这个例子需要在My Mac中执行,才能看到效果)

WX20170913-115850.png

  • 例子2:在这个例子中,我们新建了两个对象notiAnotiB来接收同一个名为reveiveNotification的通知,并且各自都会做出相应的响应。
    具体的步骤是:
    • 分别新建notiAnotiB,并且都将二者注册为接收reveiveNotification通知的观察者
    NotificationA *notiA = [[NotificationA alloc]init];
[[NSNotificationCenter defaultCenter] addObserver:notiA
                                         selector:@selector(receiveNotification)
                                             name:@"receiveNotification"
                                           object:nil];
NotificationB *notiB = [[NotificationB alloc]init];
[[NSNotificationCenter defaultCenter] addObserver:notiB
                                         selector:@selector(receiveNotification)
                                             name:@"receiveNotification"
                                           object:nil];
#import "NotificationA.h"
@implementation NotificationA
- (void)receiveNotification {
NSLog(@"Notification A receive this notification");
}
@end
    #import "NotificationB.h"
@implementation NotificationB
- (void)receiveNotification {
    NSLog(@"Notification B receive this notification");
}
@end
  • 通知中心发出名为reveiveNotification的通知的通知。
[[NSNotificationCenter defaultCenter] postNotificationName:@"receiveNotification"
                                                object:nil];

这样,notiA和notiB都会接收到这个通知,并且做出响应,如图:


image.png

因此,在程序中如果需要出发多个(其他对象中)的回调对象时,可以使用通知的方式来完成。

Blocks


上述的委托机制(Delegate)和通过机制(notification)已经能够很好的帮助程序在特定事件发生时调用制定的方法。但是他们都存在一个缺点:回调的设置代码和回调方法的具体实现通常都间隔很远,甚至出现在不同的文件中。
为了克服这个确定,我们可以通过Block对象,将回调相关的代码写在同一个代码段中。

  • 例子1. 我们在两个ViewController中进行传值。 BViewController中有一个UITextField,用户输入相应的值,我们在AViewController中进行显示。

在梳理Block回调之前,我们先要明确一点:
谁要传值谁就定义含有参数的Block, 谁要调用谁就执行这个Block
明确了这一点后,根据我们例子1中的需求,我们需要将BViewController中用户的输入传递给AViewController。因此BViewController需要定义一个Block, 然后在AViewController中进行相应的操作。
BViewController.h文件中:定义CallBackBlock

#import <UIKit/UIKit.h>
typedef void(^CallBackBlock)(NSString *text); // 定义带有参数text的block
@interface BViewController : UIViewController
@property (nonatomic, copy)CallBackBlock callBackBlock;
@end

BViewController.m文件中:将textFiled中输入的字符串传递给Block

- (IBAction)popToA:(id)sender {
   NSLog(@"text:%@",_textField.text);
   self.callBackBlock(_textField.text);
   [self.navigationController popToRootViewControllerAnimated:YES];
}

AViewController.m文件中:对BViewController传递过来的字符串进行显示

- (IBAction)getValueFromB:(id)sender {
   BViewController *vc = [[BViewController alloc]init];
   __weak AViewController *weakSelf = self; //避免循环引用
   vc.callBackBlock = ^(NSString *text) {
       weakSelf.textLabel.text = text;
   };
   [self.navigationController pushViewController:vc animated:YES];
}
  • 例子2:功能同例子1.
    其实刚看例子1的时候花了一些时间,总觉得哪里怪怪的,其实Block回调一种更常见的构建方法如下。
    BViewController.h文件中:

    // 另一种Block回调的实现方式
    - (void)passBlock:(CallBackBlock)block;
    

BViewController.m文件中:

```
// 另一种实现方式
- (void)passBlock:(CallBackBlock)block {
    block(@"这是另外一种方式的...");
}
```

AViewController.m文件中

```
- (IBAction)anotherButtonClick:(id)sender {
    BViewController *vc = [[BViewController alloc]init];
    __weak AViewController *weakSelf = self; //避免循环引用
    [vc passBlock:^(NSString *text) {
        weakSelf.anotherTextLabel.text = text;
    }];
}
```

在这个例子中,调用B的方法,将Block中包裹的变量传递给A,在A中对Block进行操作处理这个变量。

其他注意事项


无论哪种类型的回调,都应该注意避免强引用循环。常见的强引用循环的发生情况,创建的对象和回调对象之间相互拥有,导致两个对象都无法释放。
因此在构建回调方法的时候,应该遵守以下规则:

  • 通知中心不拥有观察者。如果某个对象注册成为观察者,那么通常应该在释放该对象时将其移出通知中心。
- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}
  • 对象不拥有委托对象和数据源方法。如果某个新创建的对象是一个对象的委托对象或数据源方法,那么该对象应该在其dealloc方法中取消相应的关联。
  • 对象不拥有目标。如果某个新创建的对象是另一个对象的目标,那么该对象应该再起dealloc方法中将相应的目标指针赋为nil.
  • Block对象中使用self, 应该使用weak指针避免强引用循环。
BViewController *vc = [[BViewController alloc]init];
__weak AViewController *weakSelf = self; //避免循环引用
[vc passBlock:^(NSString *text) {
    weakSelf.anotherTextLabel.text = text;
}];
  • Block对象中使用实例变量时,应该使用局部强引用。不要直接存取实例变量,使用存取方法。
BViewController *vc = [[BViewController alloc]init];
__weak AViewController *weakSelf = self; //避免循环引用
[vc passBlock:^(NSString *text) {
    weakSelf.anotherTextLabel.text = text;
    AViewController *innerSelf = weakSelf; //局部强引用
    NSLog(@"假如AViewController 存在name这个属性的话,它的值为:%@", innderSelf.name);
}];

参考资料


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,568评论 25 707
  • iOS网络架构讨论梳理整理中。。。 其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单...
    yhtang阅读 5,098评论 1 23
  • Ø什么是询价单: 询价单是采购商在阿里巴巴上发布的采购需求,包含采购产品的图文说明、采购量、采购商的联系方式、收获...
    Ali陈李港阅读 526评论 0 0
  • 文||木木君 图||网络 相逢是缘 双鱼小姐来自呼伦贝尔,是我的大学室友兼闺蜜,典型的东北大妞脾气。 双鱼小姐读...
    时糖Tsugar911阅读 1,812评论 0 3