Apple從OS X 10.4和iOS 4以后開始支持block,相對于delegate,block有很多便捷之處,使得代碼更簡潔,可讀性更強(qiáng)。但是如果使用不當(dāng),則會造成很多問題。本文結(jié)合自己的經(jīng)驗(yàn)和《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》書中的知識點(diǎn),介紹block的相關(guān)知識點(diǎn)。
block語法
我們通過以下圖來了解block的語法,圖片來自這里

我們來看看上面的圖,代碼如下
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
根據(jù)圖中的解釋,我們從左向右來看,該block返回值為int類型,'^'符號聲明一個(gè)名為myblock的block,該block有一個(gè)int類型的入?yún)?,等號右邊則為block的定義,block有一個(gè)名為num的int類型的入?yún)ⅲ?code>{return num * multiplier;};則為該block的block實(shí)現(xiàn)部分。
該block的調(diào)用方法如下,看起來像C的函數(shù)調(diào)用。
int result = myBlock(2); //reslut = 14;
我們來看看復(fù)雜一點(diǎn)的情況:
- (void)startWithBlock:(void(^)())block {
block();
}
- (void)testBlock {
NSString *strBlock = @"NSStackBlock";
[self startWithBlock:^{
NSLog(@"%@",strBlock);
}];
}
控制臺輸出
NSStackBlock
以上代碼,新手看起來可能是會有些費(fèi)勁的。我們一步一步來,首先,我們調(diào)用testBlock函數(shù),在該函數(shù)中,
^{
NSLog(@"%@",strBlock);
}];
該代碼塊實(shí)際上是傳給了startWithBlock函數(shù)的參數(shù)block,當(dāng)執(zhí)行startWithBlock函數(shù)時(shí),調(diào)用block(),實(shí)際上就是執(zhí)行了以上代碼塊。
使用typedef定義block類型
以上代碼可以通過typedef來定義block,以便閱讀,如下
typedef int (^myBlock)(int num);
在定義某個(gè)block類型時(shí),可以使用
myBlock aBlock = ^(int num) {
//Implemention
};
這樣看起來,要比之前簡單得多。
block捕獲外部變量
block內(nèi)可以訪問block之前定義的變量:
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
int result = myBlock(2); //reslut = 14;
但是,如果想在block內(nèi)部改變multiplier的值,編輯器則會報(bào)錯(cuò)
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
multiplier = 5;
return num * multiplier;
};
編輯器會提示: 變量不能被賦值,需要加上__block修飾符
error: variable is not assignable (missing __block type specifier)
此時(shí),需要將該變量使用__block修飾:
__block int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
multiplier = 5;
return num * multiplier;
};
如果multiplier變量是static、static global或者global變量,則不需要添加__block,該值也是可以在block內(nèi)部修改的。
static int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
multiplier = 5;
return num * multiplier;
};
因?yàn)閟tatic、static global或者global變量都是存儲在內(nèi)存中的全局區(qū)(靜態(tài)區(qū)),對于這三種類型變量,block內(nèi)部是捕獲了其指針,則可以直接訪問修改;而對于之前的臨時(shí)變量,block則只是捕獲了該變量的值,無法修改到外部的變量。
block內(nèi)部還可以訪問類的實(shí)例變量和self變量
@interface EOCClass : NSObject
@property (nonatomic, copy) NSString *anInstanceVariable;
@end
@implementation EOCClass
- (void)anInstanceMethod {
void (^someBlock)() = ^ {
self.anInstanceVariable = @"Something";
};
someBlock();
NSLog(@"self.aninstanceVaraible = %@", self.anInstanceVariable);
//self.aninstanceVaraible = Something
}
@end
block的內(nèi)部結(jié)構(gòu)
block 的數(shù)據(jù)結(jié)構(gòu)定義如下(圖片來自 這里):

對應(yīng)的結(jié)構(gòu)體定義如下:
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. */
};
從上面代碼看出,一個(gè) block 實(shí)例實(shí)際上由 6 部分構(gòu)成:
-
isa指針:指向該block類型的類的指針,
每個(gè)Objective-C對象,都有一個(gè)isa指針,指向?qū)ο蟮念?,而Class里也有個(gè)isa的指針, 指向meteClass(元類)。元類保存了類方法的列表。元類也有isa指針,它的isa指針最終指向的是一個(gè)根元類(root meteClass)。根元類的isa指針指向本身。如下圖
圖片來自《Pro Multithreading and Memory Management for iOS and OS X: with ARC, Grand Central Dispatch, and Blocks》
對于block,isa指針可以指向
_NSConcreteStackBlock、_NSConcreteMallocBlock、_NSConcreteGlobalBlock這三種類型 flags:按bit位表示一些block的附加信息,比如判斷block類型、判斷block引用計(jì)數(shù)、判斷block是否需要執(zhí)行輔助函數(shù)等。
reserved:保留變量,我的理解是表示block內(nèi)部的變量數(shù)。
invoke:函數(shù)指針,指向block的實(shí)現(xiàn)代碼地址。
descriptor:指向結(jié)構(gòu)體的指針,block的附加描述信息,比如保留變量數(shù)、block的大小、copy和dispose輔助函數(shù)的函數(shù)指針指針
copy函數(shù)為當(dāng)block執(zhí)行copy操作或者當(dāng)block從棧上拷貝到堆上時(shí)調(diào)用,dispose函數(shù)則是block在堆上釋放時(shí)調(diào)用。variables:block內(nèi)部捕獲的對象,如
void (^blk)(void) = ^{print(fmt,val)};
此時(shí),variables中則為fmt和val這兩個(gè)變量
block的類型
block有_NSConcreteStackBlock、_NSConcreteMallocBlock、_NSConcreteGlobalBlock這三種類型。
三種block在內(nèi)存中存儲位置如下圖

block類型的區(qū)分
以下情況,block為_NSConcreteGlobalBlock類型
- block內(nèi)部只使用了全局變量
- block內(nèi)部沒有使用任何外部的局部變量
除了以上兩種情況,其他的block為_NSConcreteStackBlock類型。
而對于_NSConcreteMallocBlock,只有當(dāng)_NSConcreteStackBlock類型的block執(zhí)行copy操作(手動或者系統(tǒng)執(zhí)行)時(shí),該block才會是_NSConcreteMallocBlock類型
我們來看看代碼,直觀的看看這三種類型的block
-
_NSConcreteGlobalBlock
由類型名字可以得知,該block是存儲在在內(nèi)存中全局區(qū)的。
void (^block)() = ^{
NSLog(@"This is a global block");
};
NSLog(@"%@",block);
控制臺輸出
<__NSGlobalBlock__: 0x104fd52d0>
或者
int globalVal = 1; //此處為全局變量
int (^myBlock)(int) = ^(int num) {
return num * globalVal;
};
NSLog(@"%@",block);
控制臺輸出
<__NSGlobalBlock__: 0x104fd5310>
該block所需要的全部信息都能在編譯期確定。該block是全局存在的,相當(dāng)于單例了。
-
_NSConcreteStackBlock
由該類型的名字可以看出,該block所占的內(nèi)存區(qū)域是分配在棧(stack)中的。也就是說,塊只在定義它的那個(gè)范圍內(nèi)(作用域)內(nèi)有效。如下面代碼:
int multiplier = 7;
NSLog(@"%@",^(int num) {
return num * multiplier;
};);
控制臺輸出
<__NSStackBlock__: 0x7fff59615a18>
以上代碼,block內(nèi)部捕獲了multiplier這個(gè)外部的局部變量,所以是_NSConcreteStackBlock類型。
因?yàn)樵揵lock存在在棧上,在超過block的作用域時(shí),該block就會被系統(tǒng)釋放,就有可能會出現(xiàn)block內(nèi)部的代碼還沒有走完,就被釋放掉的情況。對于這種情況,應(yīng)該對block執(zhí)行copy操作,將block復(fù)制到堆上。
注:在ARC下,系統(tǒng)在大部分情況下,會將block從棧上復(fù)制到堆上,這個(gè)后面會細(xì)說
-
_NSConcreteMallocBlock
對以上代碼中的block執(zhí)行copy操作,block就變成了_NSConcreteMallocBlock類型,如下
int multiplier = 7;
NSLog(@"mallocBlock:%@",[^(int num) {
return num * multiplier;
} copy]);
控制臺輸出
<__NSMallocBlock__: 0x6000000486a0>
拷貝到堆后,block的生命周期就與一般的OC對象一樣了。
ARC 下 block 的自動拷貝和手動拷貝
ARC下,以下幾種情況,系統(tǒng)會將block從棧上自動復(fù)制到堆上
- 當(dāng) block 作為函數(shù)返回值返回時(shí);
- 當(dāng) block 被賦值給
__strong修飾的 id 類型的對象或 block 對象時(shí); - 當(dāng) block 作為參數(shù)被傳入方法名帶有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 時(shí)(比如使用NSArray的enumerateObjectsUsingBlock和GCD的dispatch_async方法時(shí),其block不需要我們手動執(zhí)行copy操作)
注:系統(tǒng)方法內(nèi)部對block進(jìn)行了copy操作
因?yàn)樵贏RC下,對象默認(rèn)是用__strong修飾的,所以大部分情況下編譯器都會將 block從棧自動復(fù)制到堆上,除了以下情況
- block 作為方法或函數(shù)的參數(shù)傳遞時(shí),編譯器不會自動調(diào)用 copy 方法;
- block 作為臨時(shí)變量,沒有賦值給其他block
看看代碼
block作為函數(shù)的返回值,如下
- (void(^)())blockReturn {
NSString *strBlock = @"NSMallocBlock";
return ^(){
NSLog(@"%@",strBlock);
};
}
NSLog(@"%@",[self blockReturn]);
控制臺輸出
<__NSMallocBlock__: 0x7fa161f081f0>
block賦值給強(qiáng)引用block
typedef void(^block)();
NSString *strBlock = @"NSMallocBlock";
block mallocBlock = ^(){
NSLog(@"%@",strBlock);
};
NSLog(@"%@",mallocBlock);
控制臺輸出
<__NSMallocBlock__: 0x7fedd0d26110>
將block作為臨時(shí)變量
NSString *strBlock = @"NSStackBlock";
NSLog(@"%@",^(){
NSLog(@"%@",strBlock);
});
控制臺輸出
<__NSStackBlock__: 0x7fff563aa9b0>
block作為函數(shù)參數(shù)
- (void)startWithBlock:(void(^)())block {
NSLog(@"%@",block);
}
- (void)testBlock {
NSString *strBlock = @"NSStackBlock";
[self startWithBlock:^{
NSLog(@"%@",strBlock);
}];
}
執(zhí)行testBlock方法,控制臺輸出
<__NSStackBlock__: 0x7fff563aa988>
此處可能會有疑問:既然當(dāng)block作為函數(shù)參數(shù)時(shí)為_NSConcreteStackBlock類型,超出其作用域時(shí),block會被釋放掉,那會不會出現(xiàn)函數(shù)先退出了,block還是沒有執(zhí)行完畢的?
經(jīng)過我測試,我發(fā)現(xiàn),其實(shí)在函數(shù)中,在block執(zhí)行完畢前,函數(shù)是不會退出的。因?yàn)楹瘮?shù)中按順序執(zhí)行的,函數(shù)中block后的代碼會等待block執(zhí)行完畢,所以在block塊代碼未執(zhí)行完畢時(shí),該函數(shù)不會退出,從而沒有超過block的作用域,block不會被釋放??聪旅娴睦泳涂梢悦靼琢恕?/p>
- (void)startWithBlock:(void(^)())block {
block();
NSLog(@"%@",block);
}
- (void)testBlock {
NSString *strBlock = @"NSStackBlock";
[self startWithBlock:^{
NSLog(@"%@",strBlock);
}];
}
控制臺輸出
NSStackBlock
<__NSStackBlock__: 0x7fff54ba4a20>
從打印結(jié)果可以看出,當(dāng)我們執(zhí)行startWithBlock函數(shù)時(shí),先是執(zhí)行了block內(nèi)的代碼,再是執(zhí)行函數(shù)中block后的代碼,所以可以保證block執(zhí)行完畢。
可能,還有人會問,如果把block()放在子線程中執(zhí)行呢,這樣就不是按順序執(zhí)行了,在block塊代碼執(zhí)行之前,函數(shù)就退出了,這樣是不是block就不能執(zhí)行完畢呢?
其實(shí),把block放子線程中,無非是通過GCD和performSelectorInBackground方法,系統(tǒng)會自動GCD的block進(jìn)copy操作,而performSelectorInBackground需要傳一個(gè)selector,又相當(dāng)于走進(jìn)了函數(shù)里,還是按順序執(zhí)行了,函數(shù)還是會等待block執(zhí)行完畢。
針對不同block類型的copy、retain、release操作
- 對block不管是retain、copy、release都不會改變引用計(jì)數(shù)retainCount,retainCount始終是1;
- 針對NSConcreteGlobalBlock:retain、copy、release操作都無效;
- 針對NSConcreteStackBlock:retain、release操作無效
注意的是,NSConcreteStackBlock離開其作用域后,該block內(nèi)存將被回收,即使retain也沒用。容易犯的錯(cuò)誤是[[mutableAarry addObject:stackBlock],在stackBlock離開其作用域失效后,從mutableAarry中取到的stackBlock已經(jīng)被回收,變成了野指針。正確的做法是先將stackBlock copy到堆上,然后加入數(shù)組:[mutableAarry addObject:[stackBlock copy]]。 - NSConcreteMallocBlock支持retain、release,雖然retainCount始終是1,但內(nèi)存管理器中仍然會增加、減少計(jì)數(shù)。copy之后不會生成新的對象,只是增加了一次引用,類似retain;
注:盡量不要對block使用retain操作。因?yàn)閺纳峡梢钥闯?,retain操作對)_NSConcreteStackBlock并沒有效果,這樣會誤以為retain生效了,在后續(xù)調(diào)用block的時(shí)候,其實(shí)block早就被釋放了,從而導(dǎo)致crash
block循環(huán)引用問題
可以使用__weak、__unsafe_unretained、__block修飾詞修飾被block持有的對象來打破循環(huán),還有就是在block執(zhí)行完畢的時(shí)候,將block置nil的方法。具體細(xì)節(jié)這里就不講了,有興趣的童鞋可以看看我簡書上寫了另一篇文章。
