深入理解Block

Block 簡介

Mac OS X系統10.4及其iOS 4.0後引入了閉包的概念,這項語言特性是作爲擴展而加入GCC編譯器的。在Foundation框架中大量使用了Block。
塊就是一個實現某個功能的函數閉包,這個函數閉包可以帶有參數,也可以沒有參數,可以有返回值也可以沒有返回值者,用符號’^’來表示。塊在聲明的範圍內,可以調用塊外部的全局變量和局部變量。

void (^someBlock) () = ^{
    //A simple block
    //Implementation: some code
    //無返回值,無參數
}

void (^block) (int a, bool b) = ^(int a, bool b) {
   //some code
   //無返回值,帶參數
}

int (^block2) (int a, bool b) = ^(int a, bool b) {
   //some code
   //帶參數帶返回值
   return integerValue;
}

需要主意的是,block內部不能改變外部變量,想要在Block中改變變量的值,那麼我們只需要在變量聲明的時候加上__Block修飾符。

__block int a = 0;
void (^block)() = ^{
    a = 33;
};

Block 的內部結構

每個Object-c變量都佔據着某個內存區域,block本身也是一個對象,在存放block對象的內存區域中,首個變量是指向class的指針isa,其餘內存裏包含着對象的其他所有信息。
這裏寫圖片描述
- isa 指針,所有對象都有該指針,用於實現對象相關的功能。

  • flags,用於按 bit 位表示一些 block 的附加信息,本文後面介紹 block copy 的實現代碼可以看到對該變量的使用。
  • reserved,保留block函數代碼內的變量。
  • invoke,函數指針,指向具體的 block 實現的函數調用地址。在內存佈局中最重要的就是invoke函數指針,指向block的實現代碼
  • descriptor,是指向結構體的指針,每個塊裏都包含此結構體。block將所捕獲的變量指針拷貝到descriptor變量後。表示 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函數的指針。
  • variables,capture 過來的變量,block 能夠訪問它外部的局部變量,就是因爲將這些變量(或變 量的地址)複製到了結構體中。

對於 block 外的變量引用,block 默認是將其複製到其數據結構中來實現訪問的。對於用 __block 修飾的外部變量引用,block是複製其引用地址來實現訪問

全局block/堆block/棧block

定義block的時候,其所佔的內存區域是分配在棧中的。
在 Objective-C 語言中,一共有 3 種類型的 block:

  • _NSConcreteGlobalBlock 全局的靜態 block,不會訪問任何外部變量
  • _NSConcreteStackBlock 保存在棧中的 block,當函數返回時會被銷燬
  • _NSConcreteMallocBlock 保存在堆中的 block,當引用計數爲 0 時會被銷燬

下面的這段代碼在執行的時候就很危險:

  void (^block)();
  int a = 3;
  if (a > 0) {
     block = ^ { NSLog(@"Block A") };
  } else {
     block = ^ { NSLog(@"Block B") };
  }

在定義if else 語句中的兩個block都分配在棧內存區域,編譯器會給每個block分配好內存,然而等離開相應的範圍後,編譯器有可能會把分配給塊的內存覆蓋掉。於是這兩個塊只能保障在對應的if else語句範圍內有效,這樣的代碼運行起來就會出現問題。
爲解決此問題,可以給block對象發送copy消息以拷貝到堆空間裏。一旦複製到堆上,block就成了帶引用計數器的對象了。後續的複製操作都不會真的執行復制,只是遞增塊對象的應用計數器。 以下代碼就是安全的:

  void (^block)();
  int a = 3;
  if (a > 0) {
     block = [^{ NSLog(@"Block A") } copy];
  } else {
     block = [^{ NSLog(@"Block B") } copy];
  }

與全局變量類似,全局塊所使用的內存區域,在編譯期就已經完全確定了,全局塊可以聲明在全局內存裏。下面就是一個簡單的全局塊:

void (^block) () = ^ {
NSLog(@"this is a global block");
}

使用Block小技巧

Tip 1 爲常見block類型創建塊,聲明變量時,要把名稱放在類型中間:

typedef  int (^BLOCKSOME) (bool flag, int value);
BLOCKSOME block = ^(bool flag, int value) {
//some code
};

Tip 2

用塊引用及其所屬對象時,不要保留閉環,防止出現return cycle。如使用weakself來防止return cycle:

  __weak  ViewController *wself = self;

定義一個wself變量並加上__weak修飾符,在Block代碼塊中,所有需要self的地方都用wself來替代。這樣就不會增加引用計數,所以Block持有self對象也就不會造成循環引用,從而造成內存泄漏。

參考文章:
http://blog.devtang.com/2013/07/28/a-look-inside-blocks/

發佈了55 篇原創文章 · 獲贊 86 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章