淺析Block閉包
簡單來說,block就是將函式及其上下文封裝起來的物件,從功能上可以把它看作是C++中的匿名函式,也可稱之為塊。
Block型別寫法:
返回值+(^塊名)+(引數)= ^(引數){ 內容 }
如下所示:
int (^myBlock)(int a, int b) = ^(int a, int b){
return a + b;
};
Block結構
Block儲存區域
Block本質上也是OC物件,所以每個Block物件也有isa指標指向它們的類物件。根據Block類物件儲存的記憶體空間的不同可分為三種不同的類,分別是:
位於全域性區的Block類:__NSGlobalBlock__
位於棧區的Block類:__NSStackBlock__
位於堆區的Block類:__NSMallocBlock__
- 全域性區Block:當Block不捕獲外部變數時,會被編譯器分配到全域性區。因為無外部變數,所以執行時不會在Block內部進行copy或dispose操作,為了削減開銷,所以在編譯時就確定了大小,即儲存在全域性區。如下:
void (^myBlock)(void)=^(void){
NSLog(@"global");
};
NSLog(@"%@",[myBlock class]);
//輸出:
//__NSGlobalBlock__
-
棧區Block:當Block捕獲了外部變數後,會被分配到棧區。但是在ARC環境下,系統會自動為生成的棧區Block進行copy操作,所以為了驗證是否是在棧區,需要採用MRC環境,在main.m檔案的編譯選項設定為:
-fno-objc-arc
後執行如下程式碼:NSString* flag=@"yes"; void (^myBlock)(void)=^(void){ NSLog(@"stack:%@",flag); }; NSLog(@"%@",[myBlock class]); //輸出: //__NSStackBlock__
-
堆區Block:在MRC模式下,用copy後,會將棧區block複製到堆區。在ARC模式下,系統自動將初始化的Block複製到堆區。
//MRC環境下: NSString* flag=@"yes"; void (^myBlock)(void)=[^(void){ NSLog(@"stack:%@",flag); } copy]; NSLog(@"%@",[myBlock class]); //輸出: //__NSMallocBlock__
Block內部結構
官方的Block定義在 Block_private.h
中,具體的原始碼:Block_private.h
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
//Block結構
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
void (*invoke)(void *, ...);
struct Block_descriptor_1 *descriptor;
// imported variables
};
- isa指標:指向類物件的指標,即根據不同分割槽指向:
__NSGlobalBlock__
、__NSStackBlock__
和__NSMallocBlock__
,但是這裡的底層isa實際上指向的是父類的結構體(C語言)即:_NSConcreteGlobalBlock
、_NSConcreteStackBlock
和_NSConcreteMallocBlock
結構體,但意義是一樣的。 - flags:型別為列舉,主要用來儲存Block的狀態資訊。
- reserved:為之後開發準備的保留資訊,暫時無用。
- invoke:函式指標,指向的是實際的功能執行函式。在invoke函式的引數中還包含了Block結構體本身,這麼做的目的是在執行時,可以從記憶體中獲取block中捕獲的變數。
- descriptor:主要儲存Block的附加資訊,其中包括佔址大小、簽名等。預設指向Block_descriptor_1結構體,當Block被copy到堆上時,則會新增Block_descriptor_2和Block_descriptor_3,新增
copy
和dispose
方法用來拷貝和銷燬捕獲的變數。
Block內部結構圖(來自於Effective-OC):
Block作用
在日常的開發中,使用Block的主要用處在以下兩個方面:
-
作為回撥的方式之一,對比於代理模式,Block可將將分散的程式碼塊集中寫在一處編寫。因為有捕獲變數的機制,所以可以很輕鬆的訪問上下文,並且Block的程式碼是內聯的,執行效率會更高。
-
正是因為有了以上的優勢,所以在編寫非同步程式碼,作為非同步處理回撥時,在封裝時往往會採用handler塊的方式來編寫相關程式碼。
在編寫handler塊時有兩種策略,一種是在一個方法中提供提供兩個Block塊分別處理CompletionHandler和errorHandler,另外一種是隻提供一個Block塊,在Block塊中提供error引數,使用者自己來對error值進行判斷。一般我們更傾向於後者的方式,因為這樣處理資料會更加靈活
兩種Handler風格如下:
Downloader *myDownloader = [[Downloader alloc] initWithURL:url];
[myDownloader downloadWithCompletionHandler:^(NSData *onlineData){
//download success
}
failureHandler:^(NSError *error){
//handle error
}];
Downloader *myDownloader = [[Downloader alloc] initWithURL:url];
[myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded){
if(succeeded){
//download success
}
else{
//handle error
}
}];
Block記憶體洩漏
當幾個oc物件互相強引用成環時,就會導致物件永遠都不會被釋放,當這些物件的數量很大時,就會造成記憶體洩漏,從而導致整個系統crash的風險。
舉個例子:
當A類物件強引用了B類物件,B類物件強引用了C類物件,而C物件又強引用了A類物件。假設它們都在一個程式碼段中。如下圖所示:
因為a、b、c都被該程式碼段所強引用,所以retainCount初始化都為1,又因為它們互相強引用,所以在連成環的時候retainCount都變為了2。這時候在程式碼段中,無論是哪一個物件先從程式碼段中釋放,即retainCount--,都仍然還剩1。當整個程式碼段執行完後,三個類物件a、b、c的retainCount都從2減為了1,在整個系統中,再也沒有其他影響因素會讓它們的retainCount減少為0,這樣就會導致這三個物件在執行中永不釋放,從而造成記憶體洩漏。
在使用Block時也會很容易造成這個現象,當在網路非同步的handler塊中,我們通常會將當前ViewController中的某個網路資料屬性捕獲到handler中,在網路連線成功後將其進行賦值,這樣就相當於Block塊間接地強引用了當前VC,而通常來說,VC肯定會強引用下載器,而下載器中的Block塊一般也會做為其屬性進行強引用。如下圖所示:
為了解決強引用環的問題,可以通過將任意一個連線處斷開即可。
-
斷開1:基本不可能,在開發中在ViewController或者時ViewModel中都會將下載器作為屬性而非臨時變數,因為在調取過程中會一般會根據當前下載狀態來進行下一步操作。
-
斷開2:
方法一:不將_downloadHandler作為屬性,而是使用臨時Block變數,通常這麼做的情況是因為下載器類不需要多次使用該block,對於複雜的下載器,這種策略很難得以保證。
方法二:(推薦)在下載操作結束後呼叫的方法中令
self.downloadHandler = nil
,只要下載請求執行完畢,_downloadHandler屬性就不再強引用該block,就打破了強引用環。 -
斷開3:
方法一:因為Block強引用了VC的data屬性,實際上也就強引用了VC(self),所以我們可以通過:
__weak typeof(self) weakSelf=self
將當前VC,即self弱引用化,生成一個名為weakSelf的當前vc物件,然後在block中使用weakSelf.data=_data
來進行呼叫。方法二:方法一中大部分情況不會出現問題,但是當block塊中有延時操作,而對_data的處理也在延時操作當中時,就會出現問題了,例如:
[self.myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded){ if(succeeded){ //download success dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //延遲2s獲取data資料 weakSelf.data = onlineData; NSLog(@"%@",weakSelf.data); }); } else{ //handle error } }]; //假設成功從網路上獲取到data //列印為空
這時候就會發現,無論是weakSelf還是self的data屬性都為空。這就是因為在block執行完後(延時函式還未執行完),weakSelf所在的弱引用表已經被除名了,雖然延時函式還在執行。這時候當2s過後,weakSelf已經變為了nil,對nil傳送getter訊息也不會報錯,所以這裡就會出現取值為空的情況。
為了解決這一問題,只需要在block內再將weakSelf在程式碼段內部強引用化(該強引用僅限於Block內部)。例如:
[self.myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded){ if(succeeded){ //download success //將weakSelf強引用化生成該程式碼段的strong變數 __strong typeof(self) strongSelf=weakSelf; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ //延遲2s獲取data資料 //這裡使用strongSelf臨時變數 strongSelf.data = onlineData; NSLog(@"%@",strongSelf.data); }); } else{ //handle error } }];
這裡的strongSelf屬於臨時變數,會加到該程式碼段(Block內)的autoreleasepool當中,當該處程式碼段結束時會自動釋放掉,所以也就不會出現強引用情況。
方法三:使用臨時變數充當當前VC(self),如下:
__block XXXViewController* vc = self; //這裡self的retainCount會+1 [self.myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded){ if(succeeded){ //download success vc.data = onlineData; //這裡需要注意將該臨時變數置為nil,即將retainCount重新減為1 vc=nil; } else{ //handle error } }];
這裡需要注意在賦完值後必須將該臨時變數重新置為nil,即將retainCount減1,否則仍會出現強引用的問題。
方法四:將當前self作為block引數傳入,例如:
[self.myDownloader downloadWithBlock:^(NSData *onlineData, NSError * _Nullable error, BOOL succeeded, XXXViewController* vc){ if(succeeded){ //download success vc.data = onlineData; } else{ //handle error } }];
這種情況一般很少出現,因為下載器通常作為第三方提供的API,通常引數不會有當前控制類。所以這種情況只能用在自定義block當中使用。
總結
- 在ARC環境下開發,我們用到的一般都是堆Block或全域性Block,當捕獲外界變數時為堆Block,否則為全域性Block
- Block主要用於程式碼回撥以及非同步操作以降低程式碼分散程度。
- Block在捕獲變數時很容易造成迴圈引用,導致記憶體洩漏。在不確定呼叫第三方API是否在最後將block屬性置為空,或者沒有使用屬性而是臨時變數作為呼叫block,所以在不破環封裝性的原則下,將其視為未處理,然後在自己的程式碼中使用waekSelf和strongSelf方式來進行當前self的屬性進行操作,這樣就實現了在環節[3]中打破強引用環。