場景
block和delegate是iOS開發者經常用到的技術,也常常出現在各種面試題裡,你經常聽到他們之間的對比。
我的態度是每個成熟的技術並沒有明顯的優劣,不應該用誰好誰劣來評判他們,而應該看誰更適合應用場景,在合適的場合選擇合適的技術。
本篇文章將討論在 網路層呼叫和回撥 這個場景下的技術選擇。
Block回撥
一個常見的Block回撥,通常是業務程式碼呼叫請求,然後在回撥中獲得返回的資料,然後執行業務邏輯,如下:
// 業務層程式碼
- (void)blockDemo {
[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
// 處理業務邏輯程式碼
// ...
NSLog(@"Result from block:%@", data);
}];
}
複製程式碼
考慮點1:記憶體
之所以會考慮這個問題,是因為使用者使用時常常會剛進入一個頁面,就立刻點返回,此時網路請求剛發出去,資料返回有可能還沒回來,那麼這個網路請求會怎麼樣呢?
當前頁面controller能夠被pop出棧,釋放掉嗎?畢竟網路請求還沒結束
我們一一來驗證
controller銷燬
如何驗證記憶體釋放已經釋放,很簡單,首先我寫的網路請求並不是真的網路請求,他只會延時5s返回一個假的資料,用來方便模擬網路請求
- (void)requestWithParms:(NSDictionary *)parms WithResult:(ResultBlock)result {
int delay = NET_DELAY; // 預設是5s,可以傳引數改變
if ([parms valueForKey:@"delayTime"] != nil) {
delay = (int)[parms valueForKey:@"delayTime"];
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSString *resultData = @"This is a Mock Data!!";
NSLog(@"Network Finish:%@",resultData);
if (result) {
result(resultData, nil);
}
});
}
複製程式碼
再在viewcontroller的dealloc
方法裡列印log,就能知道dealloc
是否被呼叫,如果呼叫說明可以釋放
- (void)dealloc {
NSLog(@"NextPageViewController has been dealloc!")
}
複製程式碼
這樣驗證起來就是很簡單,只需要執行blockDemo
方法,然後立刻點返回,退出當前vc,等5秒後如果看結果
結果是vc可以釋放的
2018-03-25 19:36:07.345523+0800 NetworkCallback[4580:3600834] NextPageViewController has been dealloc!
複製程式碼
Block捕獲外界變數銷燬
我們再進一步想想,Block有個最大的特點是可以訪問當前的作用域,我們隨便建立一個陣列,重複上面操作,是否能夠銷燬
- (void)blockDemo {
NSArray *outsideArray = @[@1, @2, @3];
[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
// 處理業務邏輯
// ...
NSLog(@"Result from block:%@", data);
NSLog(@"outsideArray :%@", outsideArray);
}];
}
複製程式碼
列印結果:
2018-03-25 19:55:40.997535+0800 NetworkCallback[4970:3641450] NextPageViewController has been dealloc!
2018-03-25 19:55:44.831721+0800 NetworkCallback[4970:3641450] outsideArray :(
1,
2,
3
)
複製程式碼
神奇不?!
注意,vc先銷燬了,但是5s後,這個臨時變數竟然還沒有銷燬。那麼這個變數儲存在哪裡呢?留個懸念
考慮一下,如果你在Block程式碼裡訪問了一個超大的檔案,這個檔案必然是儲存記憶體的,然後此時你遇上了網路慢,介面好久沒有返回,那麼這個超大的檔案就會一直佔用記憶體
繼續,如果我在Block中訪問self呢?此時的self就是當前的controller,這時候可以銷燬嗎?
- (void)blockDemo {
NSArray *outsideArray = @[@1, @2, @3];
[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
// 處理業務邏輯
// ...
NSLog(@"Result from block:%@", data);
NSLog(@"outsideArray :%@", outsideArray);
NSLog(@"self :%@", self);
}];
}
複製程式碼
列印結果:
2018-03-25 20:04:21.012135+0800 NetworkCallback[5224:3659292] Network Finish:This is a Mock Data!!
2018-03-25 20:04:21.012476+0800 NetworkCallback[5224:3659292] Result from block:This is a Mock Data!!
2018-03-25 20:04:21.013186+0800 NetworkCallback[5224:3659292] outsideArray :(
1,
2,
3
)
2018-03-25 20:04:21.013675+0800 NetworkCallback[5224:3659292] self :<NextPageViewController: 0x7f993662cf20>
2018-03-25 20:04:21.013858+0800 NetworkCallback[5224:3659292] NextPageViewController has been dealloc!
複製程式碼
看到了嗎?dealloc是最後列印出來的,也就是說Block不返回,controller就釋放不了了!
看起來是在Block內訪問誰,誰就無法釋放啊!
有人會想這是不是就是迴圈引用呢?
請大家回憶一下:Block迴圈引用是self強引用Block,Block裡面再強引用self,這裡Block確實強引用了self,但是self並沒有強引用Block,這個Block是一個引數傳給了NetService,跟self並無關係
而且是迴圈引用的話,那麼vc會一直釋放不掉,但看上面的log,其實是可以釋放的,只是釋放的時機被延後了
但是,無論如何,我們試試換成 weakself
會怎麼樣呢?
列印結果:
2018-03-25 20:07:10.944557+0800 NetworkCallback[5294:3665537] NextPageViewController has been dealloc!
2018-03-25 20:07:15.228961+0800 NetworkCallback[5294:3665537] Network Finish:This is a Mock Data!!
2018-03-25 20:07:15.230836+0800 NetworkCallback[5294:3665537] Result from block:This is a Mock Data!!
2018-03-25 20:07:15.231074+0800 NetworkCallback[5294:3665537] outsideArray :(
1,
2,
3
)
2018-03-25 20:07:15.231190+0800 NetworkCallback[5294:3665537] weakSelf :(null)
複製程式碼
沒問題,果然換成weakself就解決了
原因是什麼?
為什麼在Block內訪問誰,誰就無法釋放呢?為什麼用weakself就解決了呢?
Block的本質是個物件
Block看起來像一個函式,其實在objectice-c中,它是個物件,之所以Block可以捕獲外部變數,正是因為它是個物件,他有自己的屬性,他用屬性強引用了外部變數,導致外部變數(就是上面的self和outsideArray)的引用計數不為0,也就不能釋放了
weakself做了什麼
當Block中訪問weakself的時候,強引用並沒有指向self,而是指向weakself,所以self可以被釋放
記憶體小結
- 使用Block無論是否有迴圈引用的可能,都要使用
weakself
,來防止vc被持有,而延遲釋放 - Block會導致物件的生命週期被延長,特別是當某些大檔案被Block訪問時,有機率導致記憶體不足
考慮點2:程式碼安全
這是基於上面的考慮,我們已經知道要用weakself
來保證controller被及時釋放,也可以在上面log中看到weakself變成了nil,此時有可能導致crash,因為我們正在操作一個nil物件
想象一個業務場景:分頁請求,你拉取了前面幾頁,比如page=3,然後去拉下一頁資料,此時網路請求尚未返回,使用者就退出當前頁面,此時
- 如果頁面能夠被釋放,那麼Block中的業務邏輯程式碼被執行嗎?
- 如果可以執行會有什麼危險?
其實第一個問題上面的log已經回答了,log之所以被列印出來,其實就是Block中的程式碼被執行了嘛。
也就是說即使controller已經銷燬,Block中的程式碼還是會被執行
第二個問題,執行了會有什麼危險
- 通常這裡會做json轉model,會做某些資料轉換,如果返回資料很大,比如是個三千多個元素的陣列,那麼勢必浪費CPU去執行,注意,此時controller已經銷燬了,執行程式碼是無意義的,這裡的CPU是確確實實的浪費掉了。
- 想像一下,此時weakself是nil,如果是分頁請求的資料,你通常是把新的資料加到某個陣列裡,然後你就crash了,因為你把nil加到陣列去了
- (void)blockDemo {
__weak typeof(self) weakSelf = self;
NSArray *outsideArray = @[@1, @2, @3];
[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
// 處理業務邏輯
// ...
[data addObject:weakSelf.pageArray]; // weakSelf是nil
}];
}
複製程式碼
因此,你必須小心翼翼,寫上的保護程式碼
- (void)blockDemo {
__weak typeof(self) weakSelf = self;
NSArray *outsideArray = @[@1, @2, @3];
[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
// 保護程式碼
if (weakSelf == nil) {
return;
}
// 處理業務邏輯
// ...
}];
}
複製程式碼
程式碼安全總結
- Block會有執行無意義程式碼的可能,浪費CPU
- Block會有操作nil物件導致crash的可能,因此要防寫程式碼
Delegate回撥
經過上面的驗證,看起來好像Block有挺多麻煩了,那麼delegate怎麼樣呢?我們也來試一試
首先是模擬網路請求,然後通過delegate回撥
- (void)requestWithParms:(NSDictionary *)parms {
int delay = NET_DELAY;
if ([parms valueForKey:@"delayTime"] != nil) {
delay = (int)[parms valueForKey:@"delayTime"];
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 判斷成功
// 判斷失敗
NSString *resultData = @"This is a Mock Data!!";
NSLog(@"Network Finish:%@",resultData);
// ***這裡加了判斷***
if (self.delegate && [self.delegate respondsToSelector:@selector(networkFinishWithSuccess:AndError:)]) {
[self.delegate networkFinishWithSuccess:resultData AndError:nil];
}
});
}
複製程式碼
注意我用***標註的註釋,這裡有個delegate的判斷,這裡保證了Block考慮點2不會出現,因為當delegate為nil的時候,絕對不會執行delegate的方法.
演示一下,程式碼如下
#pragma mark - Delegate request
- (void)delegateDemo {
self.service.delegate = self;
[self.service requestWithParms:nil];
}
#pragma mark - NetWorkDelegate
- (void)networkFinishWithSuccess:(id)data AndError:(NSError *)error {
NSLog(@"Result from Delegate:%@", data);
}
複製程式碼
驗證block存在的問題
同樣的,一進入Nextpage就立刻點返回,等5s看看程式碼會不會執行
2018-03-25 21:08:23.422103+0800 NetworkCallback[6399:3784145] NextPageViewController has been dealloc!
複製程式碼
只有一條log,說明記憶體釋放沒有問題,而且在回撥前對delegate的判斷,使得我們非常方便的得知業務層是否還存在了,而如果用Block來實現就很麻煩,在網路回撥前是無法得知的,一定要在Block裡面加判斷程式碼
總結:
- 在業務層delegate比Block更加優雅,可以在網路層回撥前就中斷邏輯,把錯誤發生的可能提前中斷,而不必進入業務層才做判斷,這是一個很好的隔斷
- 沒有延長某個物件生命週期,程式碼更加清晰,易於管理
delegate自己的問題
那麼難道delegate就沒有缺點了嗎?
多個業務層請求
之前的demo中只有一個業務層,工程中絕對不會只有一個,而NetService的delegate只能指向一個物件,豈不是隻有一個請求能夠拿到回撥,這豈不是滑天下之大稽?
當然不能這樣,如果使用delegate,就必須對每個請求封裝成一個物件,而不能統一的用一個NetService
@interface RequestAPI : NSObject
@property (nonatomic, weak, nullable) id<NetWorkDelegate> delegate;
/**
模擬Delegate請求方法
@param parms 請求引數
*/
- (void)requestWithParms:(NSDictionary *)parms;
複製程式碼
可是每個物件都去實現一篇請求邏輯豈不是很傻?!
所以底層還是呼叫NetService
#import "RequestAPI.h"
@implementation RequestAPI
- (void)requestWithParms:(NSDictionary *)parms {
__weak typeof(self) weakSelf = self;
[[NetService alloc] requestWithParms:parms WithResult:^(id _Nonnull data, NSError * _Nonnull error) {
if (weakSelf && [weakSelf respondsToSelector:@selector(networkFinishWithSuccess:AndError:)]) {
[self.delegate networkFinishWithSuccess:resultData AndError:nil];
}
}];
}
複製程式碼
總結
所以其實,使用delegate和Block的結合使用,由此我們可以看出
- Block適合做集約型呼叫,每個業務邏輯不一樣,但是我們可以通過把程式碼封裝在Block中,然後發給統一的方法來處理,實現了統一方法處理不同的邏輯
- delegate適合離散型呼叫,每次返回是同樣的邏輯
- 網路層呼叫要delegate和Block結合使用,在業務層回撥適合delegate,在底層網路處理適合Block