淺析Block閉包

淺析Block閉包

簡單來講,block就是將函數及其上下文封裝起來的對象,從功能上能夠把它看做是C++中的匿名函數,也可稱之爲塊。html

Block類型寫法:objective-c

返回值+(^塊名)+(參數)= ^(參數){ 內容 }網絡

以下所示:閉包

int (^myBlock)(int a, int b) = ^(int a, int b){
    return a + b;
};

Block結構

Block存儲區域

Block本質上也是OC對象,因此每一個Block對象也有isa指針指向它們的類對象。根據Block類對象存儲的內存空間的不一樣可分爲三種不一樣的類,分別是:app

位於全局區的Block類:__NSGlobalBlock__異步

位於棧區的Block類:__NSStackBlock__函數

位於堆區的Block類:__NSMallocBlock__ui

  • 全局區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後運行以下代碼:url

    NSString* flag=@"yes";
    void (^myBlock)(void)=^(void){
        NSLog(@"stack:%@",flag);
    };
    NSLog(@"%@",[myBlock class]);
    
    //輸出:
    //__NSStackBlock__
  • 堆區Block:在MRC模式下,用copy後,會將棧區block複製到堆區。在ARC模式下,系統自動將初始化的Block複製到堆區。spa

    //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,新增copydispose方法用來拷貝和銷燬捕獲的變量。

Block內部結構圖(來自於Effective-OC):

Block做用

在平常的開發中,使用Block的主要用處在如下兩個方面:

  1. 做爲回調的方式之一,對比於代理模式,Block可將將分散的代碼塊集中寫在一處編寫。由於有捕獲變量的機制,因此能夠很輕鬆的訪問上下文,而且Block的代碼是內聯的,運行效率會更高。

  2. 正是由於有了以上的優點,因此在編寫異步代碼,做爲異步處理回調時,在封裝時每每會採用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]中打破強引用環。
相關文章
相關標籤/搜索