Blocks是C語言的擴充功能:帶有自動變量(局部變量)的匿名函數。
顧名思義,所謂的匿名函數就是不帶有名稱的函數。c語言的標准不允許存在這樣的函數。例如:
int func (int count);
int resule = func(10);
如果想使用函數指針來代替直接調用函數,那麼似乎不用知道函數名也能夠使用該函數
int func (int count);
int (*funcptr)(int) = &func;
int result = (*funcptr)(10);
C語言函數中可能使用的變量:
自動變量(局部變量); 函數的參數; 靜態變量(靜態局部變量); 靜態全局變量; 全局變量;(1)、^ 返回值類型 參數列表 表達式;
^void (int i){ printf("%d", i);}
(2)、^ 參數列表 表達式;
^(int i){printf("%d", i);}
(3)、^ 表達式;
^{printf("123");}
當你省略返回值類型的時候,你的表達式裡return返回什麼類型,那麼你的返回值類型就是什麼。
當你不適用參數的時候,(void) 參數列表可以省略。
int (^myBlock) (int) = ^(int num){
return num * num;
};
說明:
(1)、int:返回值類型,如果沒有返回值則為void (2)、(^myBlock):塊定義需要有一個^標記,myBlock是塊名稱 (3)、(int):參數類型列表,如果沒有參數則為void (4)、^(int num):以^開頭的參數列表,如果沒有則為void,也可以省略 (5)、{}:block體(相當於函數體)(1)、使用block的時候,我們可以聲明block變量,他同c中的函數指針:
int f(int a) {
return a;
}
int (*fa)(int) = &f;
(2)、使用Block語法將Block賦值為Block類型變量;
int (^blk)(int) = ^(int a){return a;};
(3)、由^開始的Block語法生成的Block被賦值給變量blk中。因為與通常的變量相同,所有當然也可以由Block類型變量向Block類型變量賦值;
int (^blk1)(int) = blk;
int (^blk2)(int);
blk2 = blk1;
(4)、在函數參數中使用Block類型的變量可以向函數傳遞Block
void func (int (^blk) (int)){
NSLog(@"%d",blk(1));
}
(5)、在函數返回值中指定Block類型,可以將Block作為函數的返回值返回
int (^funk())(int){
return ^(int count){return count + 1;};
}
由此可見,在函數參數和返回值中使用Block類型變量時,記述方式極為復雜。這時,我們可以像使用函數指針類型時那樣,使用typedef來解決該問題:
typedef int (^blk_t)(int);
- (void)viewDidLoad {
[super viewDidLoad];
int myCount = 1;
int (^blk)(void) = ^{
NSLog(@"%d",myCount);
return myCount;
};
myCount ++;
blk();
NSLog(@"%d",myCount);
}
打印結果如下:
2016-05-20 15:26:43.499 class_01[42917:1552743] 1
2016-05-20 15:26:43.500 class_01[42917:1552743] 2
Blocks中,Block表達式截獲所使用的自動變量的值,即保存該自動變量的瞬間值。因為Block表達式保存了自動變量的值,所以在執行Block語法後,即使改寫Block中使用的自動變量的值也不會影響Block執行自動變量的值。
實際上,自動變量值截獲只能保存執行Block語法的瞬間的值。保存後就不能改寫該值。若嘗試改寫截獲的自動變量值,會出現編譯錯誤。如下:
int val = 0;
void (^blk)(void) = ^{
val = 1;
}
若想在Block語法的表達式中將值賦給在Block語法外聲明的自動變量,需要在該自動變量上附加__block說明符。
- (void)viewDidLoad {
[super viewDidLoad];
__block int myCount = 1;
int (^blk)(void) = ^{
NSLog(@"%d",myCount);
myCount = 4;
return myCount;
};
myCount ++;
blk();
NSLog(@"%d",myCount);
}
2016-05-20 15:33:30.154 class_01[42969:1556701] 2
2016-05-20 15:33:30.155 class_01[42969:1556701] 4
注意:
使用附有__block說明符的自動變量可在Block中賦值,該變量稱為__block變量。
由上述可得如果將值賦值給Block中截獲的自動變量,就會產生編譯;
如果截獲Objective-C對象,調用變更該對象的方法如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableArray* arr = [NSMutableArray array];
void (^blk)(void) = ^{
[arr addObject:@"1"];
};
blk();
NSLog(@"%@",arr);
}
打印:
2016-05-20 16:10:02.051 class_01[43053:1570116] (
1
)
arr是一個指針,指向一個可變長度的數組。在block裡面,並沒有修改這個指針,而是修改了這個指針指向的數組。換句話說,arr是一個整數,保存的是一塊內存區域的地址,在block裡,並沒有改變這個地址,而是讀取出這個地址,然後去操作這塊地址控件的內容。這是允許的,因為聲明的block的時候實際上是把當時的臨時變量又復制了一份,在block裡即使修改這些復制的變量,也不影響外面的原始變量,這就是所謂的閉包。但是當變量是一個指針的時候,block裡只是復制了一份這個指針,兩個指針指向同一個地址。所以在block裡面對指針指向內容做的修改,再block外面也一樣生效。
Blocks默認不能修改相同作用域范圍內的變量,但是如果這個相同作用域的變量如果使用了__block關鍵字進行修飾,則可以通過blocks進行修改。
Block是”帶有自動變量值的匿名函數”。
在實際編譯時無法轉換成我們能夠理解的源代碼,但是clang(LLVM編譯器)具有轉換為我們可讀源代碼的功能。通過”-rewrite-objc”選項就能將含有Block語法的源代碼轉換為C++的源代碼。
首先進入該源文件的絕對路徑;
clang -rewrite-objc 源代碼文件名
我們先來轉換一個簡單的block代碼:
int main() {
void (^blk)(void) = ^{
printf("Block\n");
};
blk();
return 0;
}
生成一個block.cpp的文件
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Block\n");
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main() {
void (*blk)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
可以通過轉換後的源代碼看到,通過blocks使用的匿名函數,實際上被作為簡單的c語言函數來處理。
在轉換後的代碼的命名上,是根據block語法所屬的函數名和該block語法在該函數出現的順序值。
來看block的語法:
^{printf();};
對應的是
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf();
}
這裡的__cself相當於c++中的this。
函數參數聲明:struct __main_block_impl_0 *__cself
與c++的this和oc的self相同,參數__cself 是 __main_block_impl_0結構體的指針。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
};
這裡是取出構造函數的代碼,
其中impl是__block_impl結構體。
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
Desc是指針,是__main_block_desc_0結構體的。
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
下面初始化含有這些結構體的__main_block_impl_0結構體的構造函數;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
_NSConcreteStackBlock用於初始化__block_impl結構體的isa成員。
main中構造函數是如何調用的:
void (*blk)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
拆分:
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;
這樣就容易理解了。 該源代碼在棧上生成__main_block_impl_0結構體實例的指針。我們將棧上生成的__main_block_impl_0結構體實例的指針賦值給__main_block_impl_0結構體指針類型的變量blk;
注意到生成tmp的函數的參數, 第一個參數是由block語法轉換的c語言函數指針,第二個參數是作為靜態全局變量初始化的__main_block_desc_0結構體實例指針。
看一下結構體初始化:
__main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};
這裡使用__main_block_impl_0結構體實例的大小進行初始化。
下面看看棧上的_main_block_impl_0結構體實例(即Block)是如何根據這些參數進行初始化的。如果展開__main_block_impl_0結構體的__block_impl結構體,如下:
struct __main_block_impl_0 {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
struct __main_block_desc_0* Desc;
}
該結構體根據構造函數:
isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
Block中所使用的被截獲自動變量就如:帶有自動變量值的匿名函數。 所說的,僅截獲自動變量的值。
Block中使用自動變量後,在Block的結構體實例中重寫該自動變量也不會改變原先截獲的自動變量。
如果在Block中視圖改變自動變量,將引起編譯錯誤。
但是這樣,我們就不能再block中保存值了。
解決方案有兩種方法:
(1)、c語言中有一個變量,允許Block改寫值。具體如下:
在Block中,訪問靜態全局變量/全局變量沒有什麼改變,可以直接使用。
靜態變量中,轉換後的函數原本就設置在含有block語法的函數外,所以無法從變量作用域訪問。
(2)、 使用__block說明符
__block存儲域類說明符 __block storage-class-specifier
c中的存儲域類說明符:
typedef extern static auto register__block說明符類似於static、auto和register,它們用於作為靜態變量值設置到那個存儲區域中。例如,auto表示作為自動變量存儲在棧中,static表示作為靜態變量存儲再數據區中。
Block和__block變量的實質就是在棧上的結構體實例。Block轉換為Block的結構體型的自動變量,__block變量轉換為__block變量的結構體類型的自動變量。所謂的結構體類型的自動變量,即棧上生成的該結構體的實例;
其中Block也是oc的對象,該OC的類為:_NSConreteStackBlock。
類似的類還有:
(1)、_NSConcreteGlobalBlock:他與全局變量一樣,設置在程序的數據區域(.data區)中; (2)、_NSConcreteStackBlock:它的對象Block設置在棧上; (3)、_NSConcreteMallocBlock:它的對象設置在由malloc函數分配的內存塊中(堆);
void (^blk)(void) = ^{
printf("Global Block\n");
};
該Block的類為_NSConcreteGlobalBlock類。此Block即該Block用結構體實例設置在程序的數據區域中。因為在使用全局變量的地方不能使用自動變量,所以不存在對自動變量進行截獲。由此Block用結構體實例的內容不依賴執行時的狀態,所以整個程序只需要一個實例。因此將Block用結構體實例設置在於全局變量相同的數據區域中即可。
只在截獲自動變量時,Block用結構體實例截獲的值才會根據執行時的狀態變化。
只要Block不截獲自動變量,就可以將Block用結構體實例設置在程序的數據區域。
在以下情況Block為_NSConcreteGlobalBlock類對象:
NSArray *testArr = @[@"1", @"2"];
void (^TestBlock)(void) = ^{
NSLog(@"testArr :%@", testArr);
};
需要截獲自動變量的為_NSConcreteStackBlock類對象,換句話說:
除_NSConcreteGlobalBlock的Block語法生成的Block都為_NSConcreteStackBlock類對象;
分配在棧上的Block和__block變量 其所屬的變量作用域結束,該Block或者__block變量也會被廢棄。
但是Blocks提供了 將Block和__block變量從棧上復制到堆上的方法來解決這個問題, 這樣即使語法記述其作用域結束,堆上的Block也能繼續存在。
如圖:
此時,賦值到堆上的Block將_NSConcreteMallocBlock類對象寫入Block用結構體實例的成員變量isa;
impl.isa = _NSConcreteMallocBlock;
上面的情況下只是Block,而__block變量需要用結構體成員變量__fZ喎?/kf/ware/vc/" target="_blank" class="keylink">vcndhcmRpbme/ydLUyrXP1iDO3sLbX19ibG9ja7Hkwb/F5NbD1NrVu8nPu7nKx7bRyc/Ksba8xNy5u9X9yLe12LfDzspfX2Jsb2NrseTBv6GjPGJyIC8+DQrU2s/Cw+bO0sPHu+HLtaO61NpfX2Jsb2NrseTBv8Xk1sPU2rbRyc+1xNe0zKzPwqOs0rK/ydLUt8POytW7yc+1xF9fYmxvY2ux5MG/oaO0y8qxo6zWu9Kq1bvJz7XEveG5uczlyrXA/bPJ1LGx5MG/X19mb3J3YXJkaW5n1rjP8rbRyc+1xL3hubnM5cq1wP2jrLK7udzKx7TT1bvJz7XEX19ibG9ja7Hkwb+7ucrHtNO20cnPtcRfX2Jsb2NrseTBv7a8xNzV/ci3t8POyqGjPC9wPg0KPHA+QmxvY2tzzOG5qbXEuLTWxre9t6ijrMjnus6009W7yc+4tNbGtb220cnPtcSjvyBBUkPT0NCntcTKsbryo6yx4NLrxve/ydLU19S2r8XQts+hozxiciAvPg0KwLS/tEJsb2Nruq/K/aO6PC9wPg0KPHByZSBjbGFzcz0="brush:java;">
typedef int (^blk_t)(int);
blk_t func(int rate) {
return ^(int count){return rate*count;};
}
此時將會返回配置在棧上的Block的函數。 即 程序執行中,從該函數返回函數調用方時變量作用域結束,因此棧上的Block也被廢棄。 雖然有問題,但是ARC的編譯如下:
blk_t func(int rate) {
//將通過Block語法生成的Block, 即配置在棧上的Block用結構體實例, 賦值給相當於Block類型的變量tmp中,此處的tmp相當於blk_t __strong tmp;
blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
//_Block_copy函數 將棧上的Block復制到堆上,復制後,將堆上的地址作為指針賦值給變量tmp,此處的objc_retainBlock相當於_Block_copy
tmp = objc_retainBlock(tmp);
//將堆上的Block作為OC對象, 注冊到autoreleasepool中,然後返回該對象。
return objc_autoreleaseReturnValue(tmp);
}
將Block作為函數返回值返回時,編譯器會自動生成復制到堆上的代碼。
對於配置在堆上的Block以及配置在程序的數據區域上的Block,調用copy方法如下:
注意:
①、Block_copy與copy等效,Block_release與release等效; ②、對Block不管是retain、copy、release都不會改變引用計數retainCount,retainCount始終是1; ③、NSGlobalBlock:retain、copy、release操作都無效; ④、NSStackBlock:retain、release操作無效,必須注意的是,NSStackBlock在函數返回後,Block內存將被回收。即使retain也沒用。容易犯的錯誤是[mutableAarry addObject:stackBlock],(補:在arc中不用擔心此問題,因為arc中會默認將實例化的block拷貝到堆上)在函數出棧後,從mutableAarry中取到的stackBlock已經被回收,變成了野指針。正確的做法是先將stackBlock copy到堆上,然後加入數組:[mutableAarry addObject:[[stackBlock copy] autorelease]]。支持copy,copy之後生成新的NSMallocBlock類型對象。 ⑤、NSMallocBlock支持retain、release,雖然retainCount始終是1,但內存管理器中仍然會增加、減少計數。copy之後不會生成新的對象,只是增加了一次引用,類似retain; ⑥、盡量不要對Block使用retain操作。Block從棧復制到堆上時,對__block變量產生的影響
(1)、如果一個Block中使用__block變量,當該Block從棧復制到堆時,使用的所有__block變量也必定配置在棧上。這些__block變量也全部被從棧復制到堆上。 此時Block持有__block變量。 即使在該Block已復制到堆的情況下,復制Block也對所使用的__block變量沒有任何影響。
(2)、在多個Block中使用__block變量時,因為最先會將所有的Block配置在棧上,所以__block變量也會配置在棧上。在任何一個Block從棧復制到堆時,__block變量也會一並從棧復制到堆並被該Block所持有。當剩下的Block從棧復制到堆時,被復制的Block持有__block變量,並增加__block變量的引用計數;
(3)、如果配置在堆上的Block被棄用,那麼它所使用的__block變量也就被釋放。
什麼時候棧上的Block賦值到堆?
調用Block的copy實例方法時; Block作為函數返回值返回時; 將Block賦值給附有__strong修飾符id類型的類或Block類型成員變量時; 在方法名中有usingBlock的Cocao框架方法或Grand Central Dispatch的API中傳遞Block時;在調用Block的copy的時候,如果Block配置在棧上,那麼該Block會從棧復制到堆。 當Block作為函數返回值的時候,將Block賦值給附有__strong修飾符id類型的類或者Block類型成員變量的時候,編譯器會自動將對象的Block作為參數,並調用_Block_copy函數。這與調用Block的copy實例方法效果相同。在方法中含有usingBlock的Cocoa框架方法或Crand Central Dispatch的API中傳遞Block時,在該方法或函數內部對傳遞過來的Block調用Block的copy實例方法或者_Block_copy函數。
也就是說,雖然從源代碼來看,在上面這些情況下棧上的Block被復制到了堆上, 但其實可歸結為_Block_copy函數被調用時Block從棧復制到堆。
相反,釋放復制到堆上的Block後,誰都不持有Block而使其被廢棄時調用dispose函數, 相當於對象的dealloc實例方法。
這樣,通過使用附有__strong修飾符的自動變量,Block中截獲的對象就能超出其變量作用域存在了。
截獲對象和使用__block變量時的不同:
對象:BLOCK_FIELD_IS_OBJECT
__block變量:BLOCK_FIELD_IS_BYREF
通過標志來區分是對象還是__block變量。
但是與copy函數持有截獲的對象,dispose函數釋放截獲的對象相同,copy函數持有所使用的__block比那裡,dispose函數釋放所使用的__block變量。
因此,Block中使用的賦值給附有__strong修飾符的自動變量的對象和復制到堆上的__block變量由於被堆上的Block所持有,因而可超出其變量作用域而存在。
在Block中使用對象類型自動變量時,除了以下3種情況,推薦調用Block的copy實例方法。
①、Block作為函數返回值返回時。 ②、將Block賦值給類的附有__strong修飾符的id類型或Block類型成員變量時。 ③、向方法名中含有usingBlock的Cocoa框架方法或Crand Central Dispatch的API中傳遞Block時。
__block id obj = [[NSObject alloc]init];
clang 轉化為
//__block變量用結構體部分
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
__strong id obj;
};
static void __Block_byref_id_object_copy_131(void* dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void**)((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void**)((vhar*)src + 40), 131);
}
//__block變量聲明部分
__Block_byref_obj_0 obj = {
0,
&obj,
0x2000000,
sizeof(__Block_byref_obj_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
[[NSObject alloc] init]
};
ARC有效時:
當Block從棧復制到堆時,使用_Block_object_assign函數,持有Block截獲的對象。 當堆上的Block被廢棄時,使用_Block_object_dispose函數,釋放Block截獲的對象;
在__block變量為附有__strong修飾符的id類型或對象類型自動變量的情形下會發生同樣的過程。當__block變量從棧復制到堆上,使用_Block_object_assign函數,持有賦值給__block變量的對象。當堆上的__block被廢棄時,使用_Block_object_dispose函數,釋放賦值給__block變量的對象。
由此可見,即使對象復制到堆上的附有__strong修飾符的對象類型__block變量中,只要__block變量在堆上存在,那麼該對象就會繼續處於被持有狀態。這與Block中使用賦值給附有__strong修飾符的對象類型自動變量的對象相同。
ARC有效時:
當我們在Block使用__strong修飾符的對象類型自動變量,那麼當Block從棧復制到堆時,該對象為Block所持有。 這樣容易引起循環引用。
typedef void (^blk_t)(void);
@interface MyObject:NSObject
{
blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
blk_ = ^{NSLog(@"self = %@", self);};
return self;
}
- (void)dealloc {
NSLog(@"dealloc");
}
@end
int main() {
id o = [[MyObject alloc] init];
NSLog(@"%@", o);
return 0;
}
源代碼中,MyObject類的dealloc實例方法一直沒有被調用;
MyObject類對象的Block類型成員變量blk_持有賦值為Block的強引用。即MyObject類對象持有Block。init實例方法中執行Block語法使用附有_strong修飾符的id類型變量self。並且由於Block語法賦值在了成員變量blk中,因此通過Block語法生成在棧上的Block此時由棧賦值到堆,並持有所使用的self。self持有Block,Block持有self。這正是循環引用。
為了避免循環引用,可聲明附有__weak修飾符的變量,並將self賦值使用。
- (id)init{
self = [super init];
id __weak tmp = self;
blk_ = ^{NSLog(@"self = %@", tmp);};
return self;
}
②、成員變量id——Block
下面代碼也會引起循環:
@interface MyObject:NSObject {
blk_t blk_;
id obj_;
}
@end
@implementation MyObject
- (id) init {
self = [super init];
blk_ = ^{NSLog(@"obj_ = %@", obj_);};
return self;
}
@end
Block中沒有self,也同樣截獲了self,引起循環。
其實在block語法中使用了obj_,其實就已經截獲了self: self->obj_。
與前面一樣,我們可以聲明__weak的臨時變量來避免循環引用;
我們還可以使用__block變量來避免循環引用:
typedef void (^blk_t)(void);
@interface MyObject:NSObject{
blk_t blk_;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
__block id tmp = self;
blk = ^{
NSLog(@"self = %@", tmp);
tmp = nil;
};
return self;
}
- (void)execBlock{
blk_();
}
- (void)dealloc{
NSLog(@"dealloc");
}
@end
int main() {
id o = [[MyObject alloc] init];
[o execBlock];
return 0;
}
這裡並沒有引起循環引用,但是如果不調用execBlock實例方法,即不執行賦值給成員變量blk_的Block,便會循環引用並引起內存洩露。原因如下:
MyObject類對象持有Block; Block持有__block變量; __block變量持有MyObject類對象。;①、使用__block變量的優點:
通過__block變量可控制對象的持有期間; 在不能使用__weak修飾符的環境中不使用__unsafe_unretained修飾符即可(不必擔心懸垂指針); 在執行Block時可動態地決定是否將nil或其他對象賦值在__block變量中。②、使用__block變量的缺點
為避免循環引用必須執行Block;