場(chǎng)景
block和delegate是iOS開發(fā)者經(jīng)常用到的技術(shù)榕堰,也常常出現(xiàn)在各種面試題里厌衔,你經(jīng)常聽到他們之間的對(duì)比蜡歹。
我的態(tài)度是每個(gè)成熟的技術(shù)并沒有明顯的優(yōu)劣赡突,不應(yīng)該用誰好誰劣來評(píng)判他們,而應(yīng)該看誰更適合應(yīng)用場(chǎng)景袁辈,在合適的場(chǎng)合選擇合適的技術(shù)菜谣。
本篇文章將討論在 網(wǎng)絡(luò)層調(diào)用和回調(diào) 這個(gè)場(chǎng)景下的技術(shù)選擇。
Block回調(diào)
一個(gè)常見的Block回調(diào)晚缩,通常是業(yè)務(wù)代碼調(diào)用請(qǐng)求尾膊,然后在回調(diào)中獲得返回的數(shù)據(jù),然后執(zhí)行業(yè)務(wù)邏輯荞彼,如下:
// 業(yè)務(wù)層代碼
- (void)blockDemo {
[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
// 處理業(yè)務(wù)邏輯代碼
// ...
NSLog(@"Result from block:%@", data);
}];
}
考慮點(diǎn)1:內(nèi)存
之所以會(huì)考慮這個(gè)問題冈敛,是因?yàn)橛脩羰褂脮r(shí)常常會(huì)剛進(jìn)入一個(gè)頁面,就立刻點(diǎn)返回卿泽,此時(shí)網(wǎng)絡(luò)請(qǐng)求剛發(fā)出去莺债,數(shù)據(jù)返回有可能還沒回來滋觉,那么這個(gè)網(wǎng)絡(luò)請(qǐng)求會(huì)怎么樣呢?
當(dāng)前頁面controller能夠被pop出棧齐邦,釋放掉嗎椎侠?畢竟網(wǎng)絡(luò)請(qǐng)求還沒結(jié)束
我們一一來驗(yàn)證
controller銷毀
如何驗(yàn)證內(nèi)存釋放已經(jīng)釋放,很簡(jiǎn)單措拇,首先我寫的網(wǎng)絡(luò)請(qǐng)求并不是真的網(wǎng)絡(luò)請(qǐng)求我纪,他只會(huì)延時(shí)5s返回一個(gè)假的數(shù)據(jù),用來方便模擬網(wǎng)絡(luò)請(qǐng)求
- (void)requestWithParms:(NSDictionary *)parms WithResult:(ResultBlock)result {
int delay = NET_DELAY; // 默認(rèn)是5s丐吓,可以傳參數(shù)改變
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
是否被調(diào)用,如果調(diào)用說明可以釋放
- (void)dealloc {
NSLog(@"NextPageViewController has been dealloc!")
}
這樣驗(yàn)證起來就是很簡(jiǎn)單券犁,只需要執(zhí)行blockDemo
方法术健,然后立刻點(diǎn)返回,退出當(dāng)前vc粘衬,等5秒后如果看結(jié)果
結(jié)果是vc可以釋放的
2018-03-25 19:36:07.345523+0800 NetworkCallback[4580:3600834] NextPageViewController has been dealloc!
Block捕獲外界變量銷毀
我們?cè)龠M(jìn)一步想想荞估,Block有個(gè)最大的特點(diǎn)是可以訪問當(dāng)前的作用域,我們隨便創(chuàng)建一個(gè)數(shù)組稚新,重復(fù)上面操作勘伺,是否能夠銷毀
- (void)blockDemo {
NSArray *outsideArray = @[@1, @2, @3];
[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
// 處理業(yè)務(wù)邏輯
// ...
NSLog(@"Result from block:%@", data);
NSLog(@"outsideArray :%@", outsideArray);
}];
}
打印結(jié)果:
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后屯阀,這個(gè)臨時(shí)變量竟然還沒有銷毀缅帘。那么這個(gè)變量存儲(chǔ)在哪里呢?留個(gè)懸念
考慮一下蹲盘,如果你在Block代碼里訪問了一個(gè)超大的文件股毫,這個(gè)文件必然是保存內(nèi)存的,然后此時(shí)你遇上了網(wǎng)絡(luò)慢召衔,接口好久沒有返回,那么這個(gè)超大的文件就會(huì)一直占用內(nèi)存
繼續(xù)祭陷,如果我在Block中訪問self呢苍凛?此時(shí)的self就是當(dāng)前的controller,這時(shí)候可以銷毀嗎兵志?
- (void)blockDemo {
NSArray *outsideArray = @[@1, @2, @3];
[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
// 處理業(yè)務(wù)邏輯
// ...
NSLog(@"Result from block:%@", data);
NSLog(@"outsideArray :%@", outsideArray);
NSLog(@"self :%@", self);
}];
}
打印結(jié)果:
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內(nèi)訪問誰霉涨,誰就無法釋放啊惭适!
有人會(huì)想這是不是就是循環(huán)引用呢笙瑟?
請(qǐng)大家回憶一下:Block循環(huán)引用是self強(qiáng)引用Block,Block里面再?gòu)?qiáng)引用self癞志,這里Block確實(shí)強(qiáng)引用了self往枷,但是self并沒有強(qiáng)引用Block,這個(gè)Block是一個(gè)參數(shù)傳給了NetService凄杯,跟self并無關(guān)系
而且是循環(huán)引用的話错洁,那么vc會(huì)一直釋放不掉,但看上面的log戒突,其實(shí)是可以釋放的屯碴,只是釋放的時(shí)機(jī)被延后了
但是,無論如何膊存,我們?cè)囋嚀Q成 weakself
會(huì)怎么樣呢窿锉? 打印結(jié)果:
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內(nèi)訪問誰嗡载,誰就無法釋放呢?為什么用weakself就解決了呢仍稀?
Block的本質(zhì)是個(gè)對(duì)象
Block看起來像一個(gè)函數(shù)洼滚,其實(shí)在objectice-c中,它是個(gè)對(duì)象技潘,之所以Block可以捕獲外部變量遥巴,正是因?yàn)樗莻€(gè)對(duì)象,他有自己的屬性享幽,他用屬性強(qiáng)引用了外部變量铲掐,導(dǎo)致外部變量(就是上面的self和outsideArray)的引用計(jì)數(shù)不為0,也就不能釋放了
weakself做了什么
當(dāng)Block中訪問weakself的時(shí)候值桩,強(qiáng)引用并沒有指向self摆霉,而是指向weakself,所以self可以被釋放
內(nèi)存小結(jié)
- 使用Block無論是否有循環(huán)引用的可能奔坟,都要使用
weakself
携栋,來防止vc被持有,而延遲釋放 - Block會(huì)導(dǎo)致對(duì)象的生命周期被延長(zhǎng)咳秉,特別是當(dāng)某些大文件被Block訪問時(shí)婉支,有幾率導(dǎo)致內(nèi)存不足
考慮點(diǎn)2:代碼安全
這是基于上面的考慮,我們已經(jīng)知道要用weakself
來保證controller被及時(shí)釋放澜建,也可以在上面log中看到weakself變成了nil向挖,此時(shí)有可能導(dǎo)致crash蝌以,因?yàn)槲覀冋诓僮饕粋€(gè)nil對(duì)象 想象一個(gè)業(yè)務(wù)場(chǎng)景:分頁請(qǐng)求,你拉取了前面幾頁何之,比如page=3跟畅,然后去拉下一頁數(shù)據(jù),此時(shí)網(wǎng)絡(luò)請(qǐng)求尚未返回帝美,用戶就退出當(dāng)前頁面碍彭,此時(shí)
- 如果頁面能夠被釋放,那么Block中的業(yè)務(wù)邏輯代碼被執(zhí)行嗎?
- 如果可以執(zhí)行會(huì)有什么危險(xiǎn)悼潭?
其實(shí)第一個(gè)問題上面的log已經(jīng)回答了庇忌,log之所以被打印出來,其實(shí)就是Block中的代碼被執(zhí)行了嘛舰褪。
也就是說即使controller已經(jīng)銷毀皆疹,Block中的代碼還是會(huì)被執(zhí)行
第二個(gè)問題,執(zhí)行了會(huì)有什么危險(xiǎn)
- 通常這里會(huì)做json轉(zhuǎn)model占拍,會(huì)做某些數(shù)據(jù)轉(zhuǎn)換略就,如果返回?cái)?shù)據(jù)很大,比如是個(gè)三千多個(gè)元素的數(shù)組晃酒,那么勢(shì)必浪費(fèi)CPU去執(zhí)行表牢,注意,此時(shí)controller已經(jīng)銷毀了贝次,執(zhí)行代碼是無意義的崔兴,這里的CPU是確確實(shí)實(shí)的浪費(fèi)掉了。
- 想像一下蛔翅,此時(shí)weakself是nil敲茄,如果是分頁請(qǐng)求的數(shù)據(jù),你通常是把新的數(shù)據(jù)加到某個(gè)數(shù)組里山析,然后你就crash了堰燎,因?yàn)槟惆裯il加到數(shù)組去了
- (void)blockDemo {
__weak typeof(self) weakSelf = self;
NSArray *outsideArray = @[@1, @2, @3];
[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
// 處理業(yè)務(wù)邏輯
// ...
[data addObject:weakSelf.pageArray]; // weakSelf是nil
}];
}
因此,你必須小心翼翼笋轨,寫上的保護(hù)代碼
- (void)blockDemo {
__weak typeof(self) weakSelf = self;
NSArray *outsideArray = @[@1, @2, @3];
[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
// 保護(hù)代碼
if (weakSelf == nil) {
return;
}
// 處理業(yè)務(wù)邏輯
// ...
}];
}
代碼安全總結(jié)
- Block會(huì)有執(zhí)行無意義代碼的可能秆剪,浪費(fèi)CPU
- Block會(huì)有操作nil對(duì)象導(dǎo)致crash的可能,因此要寫保護(hù)代碼
Delegate回調(diào)
經(jīng)過上面的驗(yàn)證翩腐,看起來好像Block有挺多麻煩了鸟款,那么delegate怎么樣呢?我們也來試一試
首先是模擬網(wǎng)絡(luò)請(qǐng)求茂卦,然后通過delegate回調(diào)
- (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];
}
});
}
注意我用***標(biāo)注的注釋,這里有個(gè)delegate的判斷组哩,這里保證了Block考慮點(diǎn)2不會(huì)出現(xiàn)等龙,因?yàn)楫?dāng)delegate為nil的時(shí)候处渣,絕對(duì)不會(huì)執(zhí)行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);
}
驗(yàn)證block存在的問題
同樣的蛛砰,一進(jìn)入Nextpage就立刻點(diǎn)返回罐栈,等5s看看代碼會(huì)不會(huì)執(zhí)行
2018-03-25 21:08:23.422103+0800 NetworkCallback[6399:3784145] NextPageViewController has been dealloc!
只有一條log,說明內(nèi)存釋放沒有問題泥畅,而且在回調(diào)前對(duì)delegate的判斷荠诬,使得我們非常方便的得知業(yè)務(wù)層是否還存在了,而如果用Block來實(shí)現(xiàn)就很麻煩位仁,在網(wǎng)絡(luò)回調(diào)前是無法得知的柑贞,一定要在Block里面加判斷代碼
總結(jié):
- 在業(yè)務(wù)層delegate比Block更加優(yōu)雅,可以在網(wǎng)絡(luò)層回調(diào)前就中斷邏輯聂抢,把錯(cuò)誤發(fā)生的可能提前中斷钧嘶,而不必進(jìn)入業(yè)務(wù)層才做判斷,這是一個(gè)很好的隔斷
- 沒有延長(zhǎng)某個(gè)對(duì)象生命周期琳疏,代碼更加清晰有决,易于管理
delegate自己的問題
那么難道delegate就沒有缺點(diǎn)了嗎?
多個(gè)業(yè)務(wù)層請(qǐng)求
之前的demo中只有一個(gè)業(yè)務(wù)層空盼,工程中絕對(duì)不會(huì)只有一個(gè)书幕,而NetService的delegate只能指向一個(gè)對(duì)象,豈不是只有一個(gè)請(qǐng)求能夠拿到回調(diào)揽趾,這豈不是滑天下之大稽台汇?
當(dāng)然不能這樣,如果使用delegate但骨,就必須對(duì)每個(gè)請(qǐng)求封裝成一個(gè)對(duì)象励七,而不能統(tǒng)一的用一個(gè)NetService
@interface RequestAPI : NSObject
@property (nonatomic, weak, nullable) id<NetWorkDelegate> delegate;
/**
模擬Delegate請(qǐng)求方法
@param parms 請(qǐng)求參數(shù)
*/
- (void)requestWithParms:(NSDictionary *)parms;
可是每個(gè)對(duì)象都去實(shí)現(xiàn)一篇請(qǐng)求邏輯豈不是很傻?奔缠!
所以底層還是調(diào)用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];
}
}];
}
總結(jié)
所以其實(shí)掠抬,使用delegate和Block的結(jié)合使用,由此我們可以看出
- Block適合做集約型調(diào)用校哎,每個(gè)業(yè)務(wù)邏輯不一樣两波,但是我們可以通過把代碼封裝在Block中,然后發(fā)給統(tǒng)一的方法來處理闷哆,實(shí)現(xiàn)了統(tǒng)一方法處理不同的邏輯
- delegate適合離散型調(diào)用腰奋,每次返回是同樣的邏輯
- 網(wǎng)絡(luò)層調(diào)用要delegate和Block結(jié)合使用,在業(yè)務(wù)層回調(diào)適合delegate抱怔,在底層網(wǎng)絡(luò)處理適合Block