Block的深入學(xué)習(xí)

(一)Block基礎(chǔ)回顧

1.Block定義

帶有局部變量的匿名函數(shù),差不多就與C語言中的函數(shù)指針類似,可以當(dāng)做參數(shù)傳來傳去,而且可以沒有名字。

2.Block語法完整的形式的Block語法如下

參數(shù)類型(^函數(shù)名)(形參) = ^(實(shí)參){ /代碼塊/};

并且與一般的C語言函數(shù)定義相比,僅有兩點(diǎn)不同:
(1)沒有函數(shù)名
(2)帶有"^"符號(hào)所以根據(jù)前面的語法格式可以寫出如下例子:
^int(int count) {return count+1};

當(dāng)然,也可以有很多的省略格式,省略返回值如下
^(int count) {return count+1};

省略返回值類型時(shí),如果表達(dá)式中有return語句時(shí),block語句的的返回值類型就使用return返回的類型;如果return中沒有返回類型,就使用void類型。再省略參數(shù)列表,參數(shù)列表和返回值都省略是最簡潔的方式,同時(shí)將參數(shù)和返回值省略如下:
^{printf("good!");}

3.Block類型變量在Block語法下,一旦使用了Block語法就相當(dāng)于生成了可賦值給Block類型變量的值,"Block"既指源代碼中的Block語法,也指由Block語法所生成的值即:

int (^blk)(int) = ^(int count){return count +1};
int (^blk1)(int) = blk;
int (^blk2)(int);
blk2 = blk1;

從上面看出,Block確實(shí)代表了一種語法,但在這里,對(duì)于blk,他也是一個(gè)Block類型變量的值。但是,當(dāng)Block作為函數(shù)的參數(shù)或者返回 值的時(shí)候若傳來傳去,寫法上難免有點(diǎn)復(fù)雜,畢竟都是那么長一串兒,此時(shí),就可以像C語言那樣使用typedef了:
typdef int(^blk_t)(int);

這時(shí),blk_t就變成了一種Block類型了,例如:
typef int(^blk_t)(int);
blk_t bk = ^(int count){return count+1};//很明顯省略了返回值

4.截獲的自動(dòng)變量(自動(dòng)變量==局部變量)

通過前面的知識(shí),我們已經(jīng)大部分理解了Block了,這里引入截獲的自動(dòng)變量,什么是截獲的局部變量?先看一段代碼:

int main() 
{
     int dmy = 256;
     int val = 10; 
     const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{printf(fmt, val);}; 
    val = 2; 
    fmt = "These values were changed. val = %d\n"; 
    blk(); 
    return 0;
}

執(zhí)行結(jié)果:val = 10

解釋:在該源代碼中,Block語法的表達(dá)式使用的是它之前聲明的自動(dòng)變量fmt 和val.Block語法中,Block表達(dá)式截獲的自動(dòng)變量,即保存該自動(dòng)變量瞬間的值。因?yàn)锽lock表達(dá)式保存了自動(dòng)變量的值,所以在執(zhí)行Block語法之后,即使改變Block中的自動(dòng)變量的值也不會(huì)影響B(tài)lock執(zhí)行時(shí)自動(dòng)變量的值。這就是所謂的截獲

5._ _block修飾符咱們來嘗試著,在Block中修改自動(dòng)變量的值:

int val = 0;
      void (^blk)(void) = ^{val = 1;};
      blk();
      printf("val = %d\n", val);
      ```
執(zhí)行結(jié)果:`error: variable is not assignable (missing __block type specifier) void (^blk)(void) = ^{val = 1;};~~~ ^`

很顯然,光這樣的話是不允許在Block內(nèi)部修改外面的自動(dòng)變量的值的。如果強(qiáng)勢(shì)要改呢,所以這會(huì)兒就該 __block出場了:若想在Block語法的表達(dá)式中將賦值給在Block語法外聲明的自動(dòng)變量,需要在該自動(dòng)變量上加上 _block修飾符,如下:
```objectivec
__block int val = 0;
  void (^blk)(void) = ^{val = 1;};
  blk();
 printf("val is %d",val);

執(zhí)行結(jié)果:val is 1

所以,使用 _block修飾的變量,就可以在Block語法內(nèi)部進(jìn)行修改了,該變量稱為 _block變量。但這里還有另一種情況,見如下代碼:

id array = [[NSMutableArray alloc] init]; 
void (^blk)(void) = ^{id obj = [[NSObject alloc] init];
[array addObject:obj]; };

這會(huì)出錯(cuò)嗎?其實(shí)是不會(huì)的,咱們?cè)谶@里是沒有向arry賦值,向他賦值才會(huì)產(chǎn)生編譯錯(cuò)誤,在這里,咱們截獲到了NSMutableArray類對(duì)象的一個(gè)結(jié)構(gòu)體指針(后面會(huì)講),咱們沒有對(duì)它賦值,只是使用而已,所以不會(huì)出錯(cuò)。

(二)Block存儲(chǔ)域

( _NSConcreteStackBlock,_NSConcreteGlobalBlock,_NSConcreteMallocBlock)

通過前面的學(xué)習(xí),了解到Block轉(zhuǎn)換為Block的結(jié)構(gòu)體類型的自動(dòng)變量,__block修飾符修飾的變量轉(zhuǎn)換為block變量的結(jié)構(gòu)體類型的自動(dòng)變量,所謂結(jié)構(gòu)體類型的自動(dòng)變量,即棧上生成的該結(jié)構(gòu)體的實(shí)例變量。:

根據(jù)咱們之前提到的,其實(shí)Block也是一種對(duì)象,并且Block的類是_NSConcreteStackBlock,和他類似的還有兩個(gè)如:

  • _NSConcreteMallocBlock
  • _NSConcreteGlobalBlock三個(gè)不同的類名稱決定了三個(gè)Block類生成的Block對(duì)象存在內(nèi)存中的位置:

到目前為止,出現(xiàn)的Block例子都是_NSConcreteStackBlock類,所以都是設(shè)置在棧上,但實(shí)際上并非是這樣,在記述全局變量的地方使用Block語法時(shí)生成的Block為_NSConcreteGlobalBlock對(duì)于Block對(duì)象分配在數(shù)據(jù)區(qū)的情況,略過分析過程,直接總結(jié):

  • 當(dāng)把Block聲明為全局變量的時(shí)候,Block分配在數(shù)據(jù)區(qū):
void (^blk)(void) = ^{printf("Global Block\n");};
int main() {}
  • Block語法表達(dá)式中不使用截獲的自動(dòng)變量的時(shí)候:
typedef int (^blk_t)(int);
for (int rate = 0; rate < 10; ++rate) { 
blk_t blk = ^(int count){return count;};
}

以上兩種情況,Block分配在數(shù)據(jù)區(qū)。最后一個(gè)問題,什么時(shí)候Block會(huì)分配在堆上呢?此時(shí)可以引出之前說的一個(gè)問題,“Block可以超出變量作用域而存在”,換句話說就是,Block倘若作為一個(gè)局部變量存在,結(jié)果他居然可以在超出作用域之后不被廢棄,同樣的,由于block修飾的變量也是放在棧上的,如果其變量作用域結(jié)束,那么block修飾符修飾的變量也應(yīng)該結(jié)束。解決方案如下:
將Block和__block修飾的變量從棧上復(fù)制到堆上來解決,將配置在棧上的Block復(fù)制到堆上,這樣即使Block語法記述的變量作用域結(jié)束時(shí),堆上的Block還可以繼續(xù)存在

復(fù)制到堆上之后,將Block內(nèi)部的isa成員變量進(jìn)行改變:
impl.isa = &_NSConcreteMallocBlock;

當(dāng)ARC有效時(shí),大多數(shù)情況下編譯器會(huì)進(jìn)行恰當(dāng)?shù)剡M(jìn)行判斷,自動(dòng)生成將棧上復(fù)制到堆上的代碼,并且最后復(fù)制到堆上的Block會(huì)自動(dòng)的加入到autoRealeasePool中,編譯器不能進(jìn)行判斷的情況便是:

向方法或函數(shù)的參數(shù)中傳遞Block時(shí)但是在向方法或函數(shù)的參數(shù)中傳遞Block時(shí)也有不需要手動(dòng)復(fù)制的情況如下:

  • Cocoa框架的方法且方法名中含有usingBlock等時(shí)
  • GCD中的API

舉個(gè)栗子:在使用NSArray類的enumeratObjectsUsingBlock實(shí)例方法以及dispatch_async函數(shù)時(shí),不用手動(dòng)復(fù)制,相反的,在NSArray類的initWithObjects實(shí)例方法上傳遞時(shí)需要手動(dòng)復(fù)制,看代碼:

- (id) getBlockArray {
    int val = 10;
    return [[NSArray alloc] initWithObjects: ^{NSLog(@"blk0:%d", val);},  ^{NSLog(@"blk1:%d", val);}, nil];
 }接下來,調(diào)用:
id obj = getBlockArray();
      typedef void (^blk_t)(void);
      blk_t blk = (blk_t)[obj objectAtIndex:0];
      ```

      blk(); 結(jié)果就是Block在執(zhí)行時(shí)發(fā)生異常,應(yīng)用程序強(qiáng)制結(jié)束,這是由于在getBlockArray函數(shù)執(zhí)行結(jié)束時(shí),棧上的Block被廢棄的緣故。而此時(shí)編譯器恰好又不能判斷是否需要復(fù)制。 注:但將Block從棧上復(fù)制到堆上是相當(dāng)消耗CPU的,當(dāng)Block設(shè)置在棧上也能夠使用時(shí),將Block從棧上復(fù)制到堆上只是在浪費(fèi)CPU資源,能少復(fù)制,盡量少復(fù)制。
將以上代碼修改一下即可運(yùn)行:

```objectivec
- (id) getBlockArray {
   int val = 10;
   return [[NSArray alloc] initWithObjects: [^{NSLog(@"blk0:%d", val);} copy], [^{NSLog(@"blk1:%d", val);} copy], nil];
}

小結(jié):



(三)block變量存儲(chǔ)域(Block移動(dòng)對(duì)block變量的影響)

使用block變量的Block從棧復(fù)制到堆上時(shí),block修飾的變量也會(huì)受到影響。

  • 1.多個(gè)Block使用一個(gè)block變量時(shí),因?yàn)闀?huì)將所有的Block配置在棧上,所以block變量也會(huì)配置在棧上,其中任何一個(gè)Block從棧復(fù)制到堆時(shí),block變量也會(huì)一并從棧復(fù)制到堆并被該Block持有,當(dāng)剩下的Block從棧復(fù)制到堆時(shí),被復(fù)制的Block會(huì)依次持有block變量,并增加__block變量的引用計(jì)數(shù)。
  • 2.在這里,讀者可以采用Objective-C的引用計(jì)數(shù)的方式來考慮。使用block變量的Block持有block變量,如果Block被廢棄,它所持有的block變量也就被釋放在這里,回到之前講到的“block變量使用結(jié)構(gòu)體成員變量forwarding的原因”,不管block變量配置在棧上還是在堆上,都能夠正確的訪問該變量(通過指針),通過Block的復(fù)制,block變量也從棧復(fù)制到堆,此時(shí)可同時(shí)訪問棧上的block變量和堆上的block變量,下面解釋一下:看代碼:
__block int val = 0;
   void (^blk)(void) = [^{++val;} copy];
   ++val;
   blk();
   NSLog(@"%d", val);
結(jié)果是:2
^{++val;}和++val;都可以轉(zhuǎn)化為++(val.__forwarding->val);

在變換Block語法的函數(shù)中,該變量val為復(fù)制到堆上的block變量結(jié)構(gòu)體實(shí)例,而另外一個(gè)(++val)與Block無關(guān)的變量val,為復(fù)制前棧上的block變量結(jié)構(gòu)體實(shí)例。但是棧上的block變量結(jié)構(gòu)體實(shí)例在block變量從棧復(fù)制到堆上時(shí),會(huì)將成員變量forwarding指針替換為復(fù)制目標(biāo)堆上的block變量用結(jié)構(gòu)體實(shí)例的地址

下面總結(jié)棧上的Block復(fù)制到堆的情況:

  • 調(diào)用Block的copy實(shí)例方法時(shí)
  • 將Block作為函數(shù)返回值時(shí)
  • 將Block賦值給附有__strong修飾符id類型的類或者Block類型成員變量時(shí)
  • 在方法名中含有usingBlock的Cocoa框架方法或者GCD的API中傳遞Block時(shí)

在調(diào)用Block的copy方法時(shí),如果Block配置在棧上,那么該Block會(huì)從棧上賦值到堆;將Block作為函數(shù)返回值時(shí)、將Block賦值給附有__strong修飾符id類型的類或者Block類型成員變量時(shí),編譯器將自動(dòng)地將對(duì)象的Block作為參數(shù)并調(diào)用_Block_copy函數(shù),這與調(diào)用Block的copy實(shí)例方法的效果相同;在方法名中含有usingBlock的Cocoa框架方法或者GCD的API中傳遞Block時(shí),在該方法或函數(shù)內(nèi)部對(duì)傳遞過來的Block調(diào)用Block的copy實(shí)例方法或者_(dá)Block_copy函數(shù)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容