聲明:未經(jīng)許可,禁止轉(zhuǎn)載。
整個(gè)項(xiàng)目的Gihub地址:https://github.com/LeeLom/CallBackDemo
回調(diào)(callback)就是將一段可執(zhí)行的代碼和一個(gè)特定的事件綁定起來(lái),當(dāng)特定的時(shí)間發(fā)生時(shí),就會(huì)執(zhí)行這段代碼。
在Objective-C中,有四種途徑可以試下回調(diào):

- 目標(biāo)-動(dòng)作對(duì)(Targe-Action): 在程序開始等待前,要求“
當(dāng)事件發(fā)生時(shí),向指定的對(duì)象發(fā)送某個(gè)特定的消息”。這里接受消息的對(duì)象是目標(biāo)(target),消息的選擇器(selector)是動(dòng)作(action). - 輔助對(duì)象(Helper Objects): 在程序開始等待前,要求“
當(dāng)事件發(fā)生時(shí),向遵守相應(yīng)協(xié)議的輔助對(duì)象發(fā)送消息”。Delegate 和 DataSource是我們常見(jiàn)的輔助對(duì)象 - 通知(Notification): 某個(gè)對(duì)象正在等待某些特定的通知。當(dāng)其中的某個(gè)通知出現(xiàn)時(shí),向指定的對(duì)象發(fā)送特定的消息。當(dāng)事件發(fā)生時(shí),相關(guān)的對(duì)象會(huì)向通知中心發(fā)布通知,然后再有通知中心將通知轉(zhuǎn)發(fā)給正在等待該通知的對(duì)象
- Blocks: 在程序開始等待前,聲明一個(gè)Block對(duì)象,當(dāng)事件發(fā)生時(shí),執(zhí)行這段Block對(duì)象。
在iOS開發(fā)中最常使用的就是輔助對(duì)象和Blocks. 下面將會(huì)通過(guò)四個(gè)例子來(lái)看一下這四種回調(diào)方式都是怎么實(shí)現(xiàn)的。
目標(biāo)-動(dòng)作對(duì) (Target-Action)
- 創(chuàng)建一個(gè)NSRunLoop對(duì)象和NSTimer對(duì)象的程序。
這個(gè)程序每隔2秒,NSTimer就會(huì)像其目標(biāo)發(fā)送指定的動(dòng)作消息。此外,在創(chuàng)建一個(gè)Logger類,這個(gè)類的實(shí)例將被設(shè)置為NSTimer對(duì)象的目標(biāo)。
//1. 目標(biāo)-動(dòng)作對(duì)
// 創(chuàng)建一個(gè)Logger的實(shí)例logger
Logger *logger = [[Logger alloc]init];
// 每隔2秒,NSTimer對(duì)象會(huì)向其Target對(duì)象logger,發(fā)送指定的消息updateLastTime:
__unused NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0
target:logger
selector:@selector(updateLastTime:)
userInfo:nil
repeats:YES];
輸出結(jié)果:

Target: logger
Action: logger對(duì)象的updateLastTime方法
- 在程序中常見(jiàn)的按鈕點(diǎn)擊事件也是這種類型。首先,我們用代碼創(chuàng)建了一個(gè)按鈕
btn,然后為這個(gè)按鈕添加他的目標(biāo)為當(dāng)前的AppDelegate(這里僅僅是為了舉例,一般我們都是用在ViewController當(dāng)中),對(duì)應(yīng)的Action為:btnClick。
// 創(chuàng)建一個(gè)按鈕
UIButton *btn = [[UIButton alloc]init];
// 為按鈕添加事件
[btn addTarget:self
action:@selector(btnClick)
forControlEvents:UIControlEventTouchUpInside];
- (void)btnClick {
NSLog(@"按鈕點(diǎn)擊事件");
}
從這種目標(biāo)-動(dòng)作的回調(diào)方式我們可以發(fā)現(xiàn),NSTimer它只負(fù)責(zé)一件事情updateLastTime,btn它只負(fù)責(zé)btnClick。也就是說(shuō),對(duì)于只做一件事情的對(duì)象,我們可以是使用目標(biāo)動(dòng)作對(duì)。
輔助對(duì)象 (Delegate/Datasource)
- 輔助對(duì)象是在iOS開發(fā)中相當(dāng)常見(jiàn)的。比如我們經(jīng)常使用的UITableView這個(gè)空間,相信大家都使用過(guò)其中的
UITableViewDelegate以及UITableViewDataSource。
self.tableView.delegate = self;
self.tableView.dataSource = self;
上面的兩行代碼,我們?cè)谀硞€(gè)ViewController當(dāng)中使用的話,意味著我們將ViewController設(shè)置成為了tableView的輔助對(duì)象。當(dāng)tableView需要更新或者是響應(yīng)某些特定的事件時(shí),就會(huì)向該ViewController發(fā)送消息。
具體發(fā)送哪些消息就看我們?cè)趺磳?shí)現(xiàn)的了,比如我們點(diǎn)擊某行需要響應(yīng)點(diǎn)擊事件時(shí),我們就需要實(shí)現(xiàn)下面這個(gè)方法:
// Called after the user changes the selection.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
關(guān)于UITableView的使用網(wǎng)上有大量的資料,在這里就不再重復(fù)了。我們?cè)谶@個(gè)部分只要明確一點(diǎn),UITableView它的回調(diào)方式是通過(guò)這種委托對(duì)象來(lái)實(shí)現(xiàn)的,而委托的對(duì)象通常是使用它的ViewController,我們需要委托對(duì)象為UITableView完成什么事情,就需要在委托對(duì)象ViewController中實(shí)現(xiàn)相應(yīng)的協(xié)議Protocol(也即delegate和datasource)。
- 下面,我們通過(guò)一個(gè)網(wǎng)絡(luò)異步下載的例子,進(jìn)一步加深了解這種輔助對(duì)象的回調(diào)。
我們使用NSURLConnection從服務(wù)器獲取數(shù)據(jù)時(shí),通常都是通過(guò)異步方式完成的,NSURLConnection通常不會(huì)一次就發(fā)送全部數(shù)據(jù),而是多次的發(fā)送塊狀數(shù)據(jù)。也就是說(shuō),我們需要在程序中不斷的響應(yīng)接受數(shù)據(jù)的事件。
因此,我們需要一個(gè)對(duì)象來(lái)幫助NSURLConnection完成這些操作。繼續(xù)前面的例子,我們使用Logger類的實(shí)例來(lái)完成。因?yàn)橐瓿?code>NSURLConnection的操作,所以Logger當(dāng)中要實(shí)現(xiàn)它的協(xié)議,在這個(gè)簡(jiǎn)單的例子中,我們只需要實(shí)現(xiàn)NSURLConnection的三個(gè)協(xié)議方法就好。
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
PS: 其中1.2是NSURLConnectionDataDelegate, 第三條是NSURLConnectionDelegate.
//2. 輔助對(duì)象
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設(shè)置為了NSURLConnection的輔助對(duì)象,因此網(wǎng)絡(luò)下載相關(guān)的信息都會(huì)在輔助對(duì)象logger中進(jìn)行響應(yīng)。
輸出的結(jié)果如下:

從上面的
UITableView和NSURLConnection的例子中我們可以發(fā)現(xiàn),輔助對(duì)象和目標(biāo)動(dòng)作對(duì)的實(shí)現(xiàn)邏輯非常相似,如果吧目標(biāo)理解為輔助對(duì)象,動(dòng)作理解為協(xié)議的話,二者幾乎是一一對(duì)應(yīng)的。但是二者的區(qū)別主要在于:當(dāng)要向一個(gè)對(duì)象發(fā)送多個(gè)回調(diào)的時(shí)候,通常選擇符合相應(yīng)協(xié)議的輔助對(duì)象;如果要向一個(gè)對(duì)象發(fā)送一個(gè)回調(diào)是,通常使用目標(biāo)動(dòng)作對(duì)。
輔助對(duì)象也常被成為委托對(duì)象delegate和數(shù)據(jù)源datasource。
通知 Notifications
上面所說(shuō)的目標(biāo)動(dòng)作對(duì)和輔助對(duì)象都是向一個(gè)對(duì)象發(fā)送消息,如果要向多個(gè)對(duì)象發(fā)送消息,那么我們就需要使用通知這種方式了。
- 例子1: 我們使用電腦的時(shí)候發(fā)現(xiàn),當(dāng)改變系統(tǒng)的失去設(shè)置時(shí),程序中的很多對(duì)象都可以知道這一變化。之所以能夠?qū)崿F(xiàn),是因?yàn)檫@些對(duì)象都可以通過(guò)通知中心將自己注冊(cè)成為觀察者
Observer。當(dāng)系統(tǒng)是時(shí)區(qū)發(fā)生改變的時(shí)候,會(huì)像通知中心發(fā)布NSSystemTimeZondeDidChangeNotification通知,然后通知中心將該通知轉(zhuǎn)發(fā)給所有注冊(cè)了該Name的觀察者。
同樣的,我們繼續(xù)在Logger這個(gè)類中繼續(xù)進(jìn)行操作。這次,我們Logger的實(shí)例注冊(cè)為觀察者,讓它能夠在系統(tǒng)的失去發(fā)生變化的時(shí)候收到相應(yīng)的通知。
[[NSNotificationCenter defaultCenter]addObserver:logger
selector:@selector(zoneChange:)
name:NSSystemTimeZoneDidChangeNotification
object:nil];
return YES;
- (void)zoneChange:(NSNotification *)note {
NSLog(@"The system time zone has changed!");
}
(這個(gè)例子需要在My Mac中執(zhí)行,才能看到效果)

- 例子2:在這個(gè)例子中,我們新建了兩個(gè)對(duì)象
notiA和notiB來(lái)接收同一個(gè)名為reveiveNotification的通知,并且各自都會(huì)做出相應(yīng)的響應(yīng)。
具體的步驟是:- 分別新建
notiA和notiB,并且都將二者注冊(cè)為接收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
- 通知中心發(fā)出名為
reveiveNotification的通知的通知。
[[NSNotificationCenter defaultCenter] postNotificationName:@"receiveNotification"
object:nil];
這樣,notiA和notiB都會(huì)接收到這個(gè)通知,并且做出響應(yīng),如圖:

因此,在程序中如果需要出發(fā)多個(gè)(其他對(duì)象中)的回調(diào)對(duì)象時(shí),可以使用通知的方式來(lái)完成。
Blocks
上述的委托機(jī)制(Delegate)和通過(guò)機(jī)制(notification)已經(jīng)能夠很好的幫助程序在特定事件發(fā)生時(shí)調(diào)用制定的方法。但是他們都存在一個(gè)缺點(diǎn):回調(diào)的設(shè)置代碼和回調(diào)方法的具體實(shí)現(xiàn)通常都間隔很遠(yuǎn),甚至出現(xiàn)在不同的文件中。
為了克服這個(gè)確定,我們可以通過(guò)Block對(duì)象,將回調(diào)相關(guān)的代碼寫在同一個(gè)代碼段中。
- 例子1. 我們?cè)趦蓚€(gè)ViewController中進(jìn)行傳值。
BViewController中有一個(gè)UITextField,用戶輸入相應(yīng)的值,我們?cè)?code>AViewController中進(jìn)行顯示。
在梳理Block回調(diào)之前,我們先要明確一點(diǎn):
誰(shuí)要傳值誰(shuí)就定義含有參數(shù)的Block, 誰(shuí)要調(diào)用誰(shuí)就執(zhí)行這個(gè)Block
明確了這一點(diǎn)后,根據(jù)我們例子1中的需求,我們需要將BViewController中用戶的輸入傳遞給AViewController。因此BViewController需要定義一個(gè)Block, 然后在AViewController中進(jìn)行相應(yīng)的操作。
在BViewController.h文件中:定義CallBackBlock
#import <UIKit/UIKit.h>
typedef void(^CallBackBlock)(NSString *text); // 定義帶有參數(shù)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文件中:對(duì)BViewController傳遞過(guò)來(lái)的字符串進(jìn)行顯示
- (IBAction)getValueFromB:(id)sender {
BViewController *vc = [[BViewController alloc]init];
__weak AViewController *weakSelf = self; //避免循環(huán)引用
vc.callBackBlock = ^(NSString *text) {
weakSelf.textLabel.text = text;
};
[self.navigationController pushViewController:vc animated:YES];
}
-
例子2:功能同例子1.
其實(shí)剛看例子1的時(shí)候花了一些時(shí)間,總覺(jué)得哪里怪怪的,其實(shí)Block回調(diào)一種更常見(jiàn)的構(gòu)建方法如下。
在BViewController.h文件中:// 另一種Block回調(diào)的實(shí)現(xiàn)方式 - (void)passBlock:(CallBackBlock)block;
在BViewController.m文件中:
```
// 另一種實(shí)現(xiàn)方式
- (void)passBlock:(CallBackBlock)block {
block(@"這是另外一種方式的...");
}
```
在AViewController.m文件中
```
- (IBAction)anotherButtonClick:(id)sender {
BViewController *vc = [[BViewController alloc]init];
__weak AViewController *weakSelf = self; //避免循環(huán)引用
[vc passBlock:^(NSString *text) {
weakSelf.anotherTextLabel.text = text;
}];
}
```
在這個(gè)例子中,調(diào)用B的方法,將Block中包裹的變量傳遞給A,在A中對(duì)Block進(jìn)行操作處理這個(gè)變量。
其他注意事項(xiàng)
無(wú)論哪種類型的回調(diào),都應(yīng)該注意避免強(qiáng)引用循環(huán)。常見(jiàn)的強(qiáng)引用循環(huán)的發(fā)生情況,創(chuàng)建的對(duì)象和回調(diào)對(duì)象之間相互擁有,導(dǎo)致兩個(gè)對(duì)象都無(wú)法釋放。
因此在構(gòu)建回調(diào)方法的時(shí)候,應(yīng)該遵守以下規(guī)則:
- 通知中心不擁有觀察者。如果某個(gè)對(duì)象注冊(cè)成為觀察者,那么通常應(yīng)該在釋放該對(duì)象時(shí)將其移出通知中心。
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- 對(duì)象不擁有委托對(duì)象和數(shù)據(jù)源方法。如果某個(gè)新創(chuàng)建的對(duì)象是一個(gè)對(duì)象的委托對(duì)象或數(shù)據(jù)源方法,那么該對(duì)象應(yīng)該在其
dealloc方法中取消相應(yīng)的關(guān)聯(lián)。 - 對(duì)象不擁有目標(biāo)。如果某個(gè)新創(chuàng)建的對(duì)象是另一個(gè)對(duì)象的目標(biāo),那么該對(duì)象應(yīng)該再起
dealloc方法中將相應(yīng)的目標(biāo)指針賦為nil. - 在
Block對(duì)象中使用self, 應(yīng)該使用weak指針避免強(qiáng)引用循環(huán)。
BViewController *vc = [[BViewController alloc]init];
__weak AViewController *weakSelf = self; //避免循環(huán)引用
[vc passBlock:^(NSString *text) {
weakSelf.anotherTextLabel.text = text;
}];
- 在
Block對(duì)象中使用實(shí)例變量時(shí),應(yīng)該使用局部強(qiáng)引用。不要直接存取實(shí)例變量,使用存取方法。
BViewController *vc = [[BViewController alloc]init];
__weak AViewController *weakSelf = self; //避免循環(huán)引用
[vc passBlock:^(NSString *text) {
weakSelf.anotherTextLabel.text = text;
AViewController *innerSelf = weakSelf; //局部強(qiáng)引用
NSLog(@"假如AViewController 存在name這個(gè)屬性的話,它的值為:%@", innderSelf.name);
}];
參考資料
- 《Objective-C編程》(第二版) 王蕾 譯. 華中科技大學(xué)出版社
- iOS 簡(jiǎn)單易懂的 Block 回調(diào)使用和解析
- ios - block數(shù)據(jù)的回調(diào)