聲明:未經許可,禁止轉載。
整個項目的Gihub地址:https://github.com/LeeLom/CallBackDemo
回調(callback)就是將一段可執(zhí)行的代碼和一個特定的事件綁定起來,當特定的時間發(fā)生時,就會執(zhí)行這段代碼。
在Objective-C中,有四種途徑可以試下回調:

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

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

從上面的
UITableView和NSURLConnection的例子中我們可以發(fā)現(xiàn),輔助對象和目標動作對的實現(xiàn)邏輯非常相似,如果吧目標理解為輔助對象,動作理解為協(xié)議的話,二者幾乎是一一對應的。但是二者的區(qū)別主要在于:當要向一個對象發(fā)送多個回調的時候,通常選擇符合相應協(xié)議的輔助對象;如果要向一個對象發(fā)送一個回調是,通常使用目標動作對。
輔助對象也常被成為委托對象delegate和數據源datasource。
通知 Notifications
上面所說的目標動作對和輔助對象都是向一個對象發(fā)送消息,如果要向多個對象發(fā)送消息,那么我們就需要使用通知這種方式了。
- 例子1: 我們使用電腦的時候發(fā)現(xiàn),當改變系統(tǒng)的失去設置時,程序中的很多對象都可以知道這一變化。之所以能夠實現(xiàn),是因為這些對象都可以通過通知中心將自己注冊成為觀察者
Observer。當系統(tǒng)是時區(qū)發(fā)生改變的時候,會像通知中心發(fā)布NSSystemTimeZondeDidChangeNotification通知,然后通知中心將該通知轉發(fā)給所有注冊了該Name的觀察者。
同樣的,我們繼續(xù)在Logger這個類中繼續(xù)進行操作。這次,我們Logger的實例注冊為觀察者,讓它能夠在系統(tǒng)的失去發(fā)生變化的時候收到相應的通知。
[[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中執(zhí)行,才能看到效果)

- 例子2:在這個例子中,我們新建了兩個對象
notiA和notiB來接收同一個名為reveiveNotification的通知,并且各自都會做出相應的響應。
具體的步驟是:- 分別新建
notiA和notiB,并且都將二者注冊為接收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都會接收到這個通知,并且做出響應,如圖:

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