網路回撥:Block和Delegate的對比

點燃火焰發表於2018-03-25

場景

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可以被釋放

記憶體小結

  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

相關文章