要了解Block需要先了解什么是“閉包性”
2.3 閉包性
上文說過,block實際是Objc對閉包的實現。
我們來看看下面代碼:
#import?void?logBlock(?int?(?^?theBlock?)(?void?)?)
{
NSLog(?@"Closure?var?X:?%i",?theBlock()?);
}
int?main(?void?)
{
NSAutoreleasePool?*?pool;
int?(?^?myBlock?)(?void?);
int?x;
pool?=?[?[?NSAutoreleasePool?alloc?]?init?];
x?=?42;
myBlock?=?^(?void?)
{
returnx;
};
logBlock(?myBlock?);
[?pool?release?];
returnEXIT_SUCCESS;
}
上面的代碼在main函數中聲明了一個整型,并賦值42,另外還聲明了一個block,該block會將42返回。然后將block傳遞給 logBlock函數,該函數會顯示出返回的值42。即使是在函數logBlock中執行block,而block又聲明在main函數中,但是 block仍然可以訪問到x變量,并將這個值返回。
注意:block同樣可以訪問全局變量,即使是static。
一,明確兩點
1,Block可以訪問Block函數以及語法作用域以內的外部變量。也就是說:一個函數里定義了個block,這個block可以訪問該函數的內部變量(當然還包括靜態,全局變量)-即block可以使用和本身定義范圍相同的變量。
2,Block其實是特殊的Objective-C對象,可以使用copy,release等來管理內存,但和一般的NSObject的管理方式有些不同,稍后會說明。
二,Block語法
Block很像函數指針,這從Block的語法上就可以看出。Block的原型:
返回值 (^名稱)(參數列表)
Block的定義
^ 返回值類型 (參數列表) { 表達式 }
其中返回值類型和參數列表都可以省略,最簡單的Block就是:
^{ ; };
一般的定義就是:
返回值 (^名稱)(參數列表) = ^(參數列表){代碼段};
為了方便通常使用typedef定義:
typedef void (^blk) (void);
三,Block存儲域
Block能夠截獲自動變量,自動變量的當前值會被拷貝到棧上作為常量,此時不能在Block內對自動變量進行賦值操作,如果有這種需求,則需要該變量是:
1,靜態變量
2,全局變量
3,或者使用__block修飾符
根據Block中是否引用了自動變量,可以將Block存儲區域分類:
1,_NSConcreteStackBlock-存儲在棧上
2,_NSConcreteGlobalBlock-存儲在全局數據區域(和全局變量一樣)
3,_NSConcreteMallocBlock-存儲在堆上
沒 有引用自動變量或者在全局作用域的Block為_NSConcreteGlobalBlock,其他的基本上都是 _NSConcreteStackBlock。對_NSConcreteStackBlock執行copy操作會生成 _NSConcreteMallocBlock。
一般來說出問題的Block大部分都是_NSConcreteStackBlock,超過了_NSConcreteStackBlock的作用域_NSConcreteStackBlock就會銷毀。
四,對Block執行retain,copy方法的效果
Block是C語言的擴展,C語法也可以使用Block的語法,對應的C語言使用Block_copy,Block_release.
無論是_NSConcreteStackBlock,還是_NSConcreteGlobalBlock,執行retain都不起作用。而_NSConcreteMallocBlock執行retain引用計數+1。
對 于copy操作,_NSConcreteStackBlock會被復制到堆上得到新的_NSConcreteMallocBlock,而 _NSConcreteGlobalBlock執行copy操作不起作用。而對_NSConcreteMallocBlock執行copy操作會引起引用 計數加1。
那么就引出一個問題,對_NSConcreteMallocBlock多次copy會不會引起問題呢?參考Objective-C高級管理115頁。
五,什么時候要對NSConcreteStackBlock執行copy操作?
配 置在棧上的Block也就是NSConcreteStackBlock類型的Block,如果其所屬的變量作用域結束該Block就會廢棄。這個時候如果 繼續使用該Block,就應該使用copy方法,將NSConcreteStackBlock拷貝為_NSConcreteMallocBlock。當 _NSConcreteMallocBlock的引用計數變為0,該_NSConcreteMallocBlock就會被釋放。
如果是非ARC環境,需要顯式的執行copy或者antorelease方法。
而當ARC有效的時候,實際上大部分情況下編譯器已經為我們做好了,自動的將Block從棧上復制到堆上。包括以下幾個情況:
1,Block作為返回值時
類似在非ARC的時候,對返回值Block執行[[returnedBlock copy] autorelease];
2,方法的參數中傳遞Block時
3,Cocoa框架中方法名中還有useringBlock等時
4,GCD相關的一系列API傳遞Block時。
比如:[mutableAarry addObject:stackBlock];這段代碼在非ARC環境下肯定有問題,而在ARC環境下方法參數中傳遞NSConcreteStackBlock會自動執行copy。
六,Block的循環引用
對于非ARC下, 為了防止循環引用, 我們使用__block來修飾在Block中使用的對象:
對于ARC下, 為了防止循環引用, 我們使用__weak來修飾在Block中使用的對象。原理就是:ARC中,Block中如果引用了__strong修飾符的自動變量,則相當于Block對該變量的引用計數+1。
2.4 block中變量的復制與修改
對于block外的變量引用,block默認是將其復制到其數據結構中來實現訪問的,如下圖:
通過block進行閉包的變量是const的。也就是說不能在block中直接修改這些變量。來看看當block試著增加x的值時,會發生什么:
myBlock?=?^(?void?)
{
x++;
returnx;
};
編譯器會報錯,表明在block中變量x是只讀的。
有時候確實需要在block中處理變量,怎么辦?別著急,我們可以用__block關鍵字來聲明變量,這樣就可以在block中修改變量了。
基于之前的代碼,給x變量添加__block關鍵字,如下:
__block?int?x;
對于用__block修飾的外部變量引用,block是復制其引用地址來實現訪問的,如下圖:
3.編譯器中的block
3.1 block的數據結構定義
我們通過大師文章中的一張圖來說明:
上圖這個結構是在棧中的結構,我們來看看對應的結構體定義:
struct?Block_descriptor?{
unsigned?long?int?reserved;
unsigned?long?int?size;
void?(*copy)(void?*dst,?void?*src);
void?(*dispose)(void?*);
};
struct?Block_layout?{
void?*isa;
int?flags;
int?reserved;
void?(*invoke)(void?*,?...);
struct?Block_descriptor?*descriptor;
/*?Imported?variables.?*/
};
從上面代碼看出,Block_layout就是對block結構體的定義:
isa指針:指向表明該block類型的類。
flags:按bit位表示一些block的附加信息,比如判斷block類型、判斷block引用計數、判斷block是否需要執行輔助函數等。
reserved:保留變量,我的理解是表示block內部的變量數。
invoke:函數指針,指向具體的block實現的函數調用地址。
descriptor:block的附加描述信息,比如保留變量數、block的大小、進行copy或dispose的輔助函數指針。
variables:因為block有閉包性,所以可以訪問block外部的局部變量。這些variables就是復制到結構體中的外部局部變量或變量的地址。
3.2 block的類型
block有幾種不同的類型,每種類型都有對應的類,上述中isa指針就是指向這個類。這里列出常見的三種類型:
_NSConcreteGlobalBlock:全局的靜態block,不會訪問任何外部變量,不會涉及到任何拷貝,比如一個空的block。例如:
#include?int?main()
{
^{?printf("Hello,?World!\n");?}?();
return0;
}
_NSConcreteStackBlock:保存在棧中的block,當函數返回時被銷毀。例如:
#include?int?main()
{
char?a?='A';
^{?printf("%c\n",a);?}?();
return0;
_NSConcreteMallocBlock:保存在堆中的block,當引用計數為0時被銷毀。該類型的block都是由 _NSConcreteStackBlock類型的block從棧中復制到堆中形成的。例如下面代碼中,在 exampleB_addBlockToArray方法中的block還是_NSConcreteStackBlock類型的,在exampleB方法中 就被復制到了堆中,成為_NSConcreteMallocBlock類型的block:
void?exampleB_addBlockToArray(NSMutableArray?*array)?{
char?b?='B';
[array?addObject:^{
printf("%c\n",?b);
}];
}
void?exampleB()?{
NSMutableArray?*array?=?[NSMutableArray?array];
exampleB_addBlockToArray(array);
void?(^block)()?=?[array?objectAtIndex:0];
block();
}
總結一下:
_NSConcreteGlobalBlock類型的block要么是空block,要么是不訪問任何外部變量的block。它既不在棧中,也不在堆中,我理解為它可能在內存的全局區。
_NSConcreteStackBlock類型的block有閉包行為,也就是有訪問外部變量,并且該block只且只有有一次執行,因為棧中的空間是可重復使用的,所以當棧中的block執行一次之后就被清除出棧了,所以無法多次使用。
_NSConcreteMallocBlock類型的block有閉包行為,并且該block需要被多次執行。當需要多次執行時,就會把該block從棧中復制到堆中,供以多次執行。
3.3 編譯器如何編譯
我們通過一個簡單的示例來說明:
#import?typedef?void(^BlockA)(void);
__attribute__((noinline))
void?runBlockA(BlockA?block)?{
block();
}
void?doBlockA()?{
BlockA?block?=?^{
//?Empty?block
};
runBlockA(block);
}
上面的代碼定義了一個名為BlockA的block類型,該block在函數doBlockA中實現,并將其作為函數runBlockA的參數,最后在函數doBlockA中調用函數runBloackA。
注 意:如果block的創建和調用都在一個函數里面,那么優化器(optimiser)可能會對代碼做優化處理,從而導致我們看不到編譯器中的一些操作,所 以用__attribute__((noinline))給函數runBlockA添加noinline,這樣優化器就不會在doBlockA函數中對 runBlockA的調用做內聯優化處理。
我們來看看編譯器做的工作內容:
#import?__attribute__((noinline))
void?runBlockA(struct?Block_layout?*block)?{
block->invoke();
}
void?block_invoke(struct?Block_layout?*block)?{
//?Empty?block?function
}
void?doBlockA()?{
struct?Block_descriptor?descriptor;
descriptor->reserved?=?0;
descriptor->size?=?20;
descriptor->copy?=?NULL;
descriptor->dispose?=?NULL;
struct?Block_layout?block;
block->isa?=?_NSConcreteGlobalBlock;
block->flags?=?1342177280;
block->reserved?=?0;
block->invoke?=?block_invoke;
block->descriptor?=?descriptor;
runBlockA(&block);
}
上面的代碼結合block的數據結構定義,我們能很容易得理解編譯器內部對block的工作內容。
3.4 copy()和dispose()
上 文中提到,如果我們想要在以后繼續使用某個block,就必須要對該block進行拷貝操作,即從棧空間復制到堆空間。所以拷貝操作就需要調用 Block_copy()函數,block的descriptor中有一個copy()輔助函數,該函數在Block_copy()中執行,用于當 block需要拷貝對象的時候,拷貝輔助函數會retain住已經拷貝的對象。
既然有有copy那么就應該有release,與 Block_copy()對應的函數是Block_release(),它的作用不言而喻,就是釋放我們不需要再使用的block,block的 descriptor中有一個dispose()輔助函數,該函數在Block_release()中執行,負責做和copy()輔助函數相反的操作,例 如釋放掉所有在block中拷貝的變量等。
4.總結
以上內容是我學習各大師的文章后對自己學習情況的一個記錄,其中有部分文字和代碼示例是來自大師的文章,還有一些自己的理解,如有錯誤還請大家勘誤。