網絡回調:Block和Delegate的對比

場景

block和delegate是iOS開發者常常用到的技術,也經常出如今各類面試題裏,你常常聽到他們之間的對比。html

個人態度是每一個成熟的技術並無明顯的優劣,不該該用誰好誰劣來評判他們,而應該看誰更適合應用場景,在合適的場合選擇合適的技術。ios

本篇文章將討論在 網絡層調用和回調 這個場景下的技術選擇。git

本文涉及代碼github

Block回調

一個常見的Block回調,一般是業務代碼調用請求,而後在回調中得到返回的數據,而後執行業務邏輯,以下:面試

// 業務層代碼
- (void)blockDemo {
	[self.service requestWithParms:nil WithResult:^(id data, NSError *error) {
	    // 處理業務邏輯代碼
	    // ...
		NSLog(@"Result from block:%@", data);
	}];
}
複製代碼

考慮點1:內存

之因此會考慮這個問題,是由於用戶使用時經常會剛進入一個頁面,就馬上點返回,此時網絡請求剛發出去,數據返回有可能還沒回來,那麼這個網絡請求會怎麼樣呢?json

當前頁面controller可以被pop出棧,釋放掉嗎?畢竟網絡請求還沒結束數組

咱們一一來驗證安全

controller銷燬

如何驗證內存釋放已經釋放,很簡單,首先我寫的網絡請求並非真的網絡請求,他只會延時5s返回一個假的數據,用來方便模擬網絡請求bash

- (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能夠被釋放

內存小結

  1. 使用Block不管是否有循環引用的可能,都要使用weakself,來防止vc被持有,而延遲釋放
  2. Block會致使對象的生命週期被延長,特別是當某些大文件被Block訪問時,有概率致使內存不足

考慮點2:代碼安全

這是基於上面的考慮,咱們已經知道要用weakself來保證controller被及時釋放,也能夠在上面log中看到weakself變成了nil,此時有可能致使crash,由於咱們正在操做一個nil對象 想象一個業務場景:分頁請求,你拉取了前面幾頁,好比page=3,而後去拉下一頁數據,此時網絡請求還沒有返回,用戶就退出當前頁面,此時

  1. 若是頁面可以被釋放,那麼Block中的業務邏輯代碼被執行嗎?
  2. 若是能夠執行會有什麼危險?

其實第一個問題上面的log已經回答了,log之因此被打印出來,其實就是Block中的代碼被執行了嘛。

也就是說即便controller已經銷燬,Block中的代碼仍是會被執行

第二個問題,執行了會有什麼危險

  1. 一般這裏會作json轉model,會作某些數據轉換,若是返回數據很大,好比是個三千多個元素的數組,那麼勢必浪費CPU去執行,注意,此時controller已經銷燬了,執行代碼是無心義的,這裏的CPU是確確實實的浪費掉了。
  2. 想像一下,此時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;
		}
		
		// 處理業務邏輯
		// ...
	}];
}
複製代碼

代碼安全總結

  1. Block會有執行無心義代碼的可能,浪費CPU
  2. 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裏面加判斷代碼

總結:

  1. 在業務層delegate比Block更加優雅,能夠在網絡層回調前就中斷邏輯,把錯誤發生的可能提早中斷,而沒必要進入業務層才作判斷,這是一個很好的隔斷
  2. 沒有延長某個對象生命週期,代碼更加清晰,易於管理

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的結合使用,由此咱們能夠看出

  1. Block適合作集約型調用,每一個業務邏輯不同,可是咱們能夠經過把代碼封裝在Block中,而後發給統一的方法來處理,實現了統一方法處理不一樣的邏輯
  2. delegate適合離散型調用,每次返回是一樣的邏輯
  3. 網絡層調用要delegate和Block結合使用,在業務層回調適合delegate,在底層網絡處理適合Block

參考: AFNetworking Casa Taloyum

相關文章
相關標籤/搜索