本文介紹一下 iOS 中 Block 相關(guān)內(nèi)容,總結(jié) Block 相關(guān)的使用方法和注意事項(xiàng)。
Block 概述
Block 也被稱(chēng)作閉包,是不帶有名稱(chēng)的函數(shù)肤京,匿名函數(shù)。相當(dāng)于是一個(gè)代碼塊茅特,把想要執(zhí)行的代碼封裝在代碼塊里忘分,等需要的時(shí)候調(diào)用。
Block 表達(dá)式語(yǔ)法:
返回值類(lèi)型(^變量名)(參數(shù)) = ^返回值類(lèi)型(參數(shù)){ 表達(dá)式 }
int (^myBlock1)(int) = ^(int num) { return num + 1; };
void (^myBlock2)(void) = ^(void){ NSLog(@"無(wú)參數(shù)白修,無(wú)返回值"); };
Block 常見(jiàn)用法
在 OC 中經(jīng)常會(huì)用到 Block妒峦。使用 Block 方便快捷,集中代碼塊熬荆,適用于輕便舟山、簡(jiǎn)潔的回調(diào),如網(wǎng)絡(luò)傳輸?shù)嚷笨摇O旅娼榻B幾種常見(jiàn)的用法:
1累盗、聲明為屬性
可將 block 聲明為某個(gè)類(lèi)的屬性,在別的地方初始化賦值之后突琳,等待觸發(fā)回調(diào)傳值等操作若债。
@property (nonatomic, copy) void(^blockName)(NSInteger type);
someObj.blockName = ^(NSInteger type) {
//...
};
//如要是覺(jué)得 block 語(yǔ)法書(shū)寫(xiě)別扭、不友好拆融,也可使用 typedef 定義一個(gè) block 類(lèi)型蠢琳,方便使用:
typedef void(^BlockName)(NSInteger);
@property (nonatomic, copy) BlockName blockName;
2啊终、作為方法參數(shù)調(diào)用
當(dāng)有時(shí)調(diào)用一個(gè)耗時(shí)的方法處理,不會(huì)立即返回傲须,需要時(shí)間進(jìn)行處理蓝牲,當(dāng)處理完成后告知調(diào)用者處理完畢,可進(jìn)行后面的操作泰讽,在這樣的情景下就可使用 block例衍,例如網(wǎng)絡(luò)請(qǐng)求回調(diào)。
- (void)doSomethingParameters:(id)parameters completion:(void (^)(NSInteger type))completion {
//...
completion(1);
}
[self doSomethingParameters:nil completion:^(NSInteger type) {
//處理完成已卸,這里可進(jìn)行后續(xù)操作...
}];
Block 的實(shí)質(zhì)
Block 其實(shí)是作為 C語(yǔ)言的代碼來(lái)進(jìn)行處理的佛玄,通過(guò)編譯器將 Block 語(yǔ)法轉(zhuǎn)換為相關(guān)的 C語(yǔ)言代碼,然后進(jìn)行編譯執(zhí)行累澡∶吻溃可以通過(guò) clang 來(lái)將 OC 代碼轉(zhuǎn)換為 C/C++ 代碼,執(zhí)行下面命令:
clang -rewrite-objc main.m
然后生成 main.cpp 文件愧哟“路裕可以看到,簡(jiǎn)單的幾行代碼翅雏,轉(zhuǎn)化為一堆 C語(yǔ)言代碼:
int main(int argc, const char * argv[]) {
@autoreleasepool {
void (^blk)(void) = ^(){ NSLog(@"Block !"); };
blk();
}
return 0;
}
上面的代碼其實(shí)就是圈驼,將 Block 的匿名函數(shù)作為 C語(yǔ)言的函數(shù)來(lái)處理。
就是使用函數(shù)指針調(diào)用函數(shù)望几,將函數(shù) __main_block_func_0(__cself) 賦值給 impl.FuncPtr, 然后調(diào)用 blk->impl.FuncPtr(blk)萤厅,參數(shù) __cself 就是指向 blk 自身的指針橄抹。
Block 就是 OC 對(duì)象
OC 中由類(lèi)生成對(duì)象,就是由該類(lèi)生成對(duì)象的各個(gè)結(jié)構(gòu)體惕味,通過(guò)對(duì)象的成員變量 isa 指針指向該類(lèi)的結(jié)構(gòu)體實(shí)例 objc_class 繼承自 objc_object楼誓,該結(jié)構(gòu)體實(shí)例持有聲明的成員變量、方法的名稱(chēng)名挥、方法的實(shí)現(xiàn)(函數(shù)指針)疟羹、屬性以及父類(lèi)的指針。具體內(nèi)容請(qǐng)參考之前的文章 Objective-C對(duì)象解析
Block 就是 OC 對(duì)象禀倔。 __main_block_impl_0 結(jié)構(gòu)體相當(dāng)于榄融,基于 objc_object 結(jié)構(gòu)體的 OC 對(duì)象的結(jié)構(gòu)體。
值得注意的是救湖,impl.isa = &_NSConcreteStackBlock;
_NSConcreteStackBlock 相當(dāng)于一個(gè)結(jié)構(gòu)體實(shí)例愧杯,將 Block 作為 OC 對(duì)象處理時(shí),關(guān)于該類(lèi)的信息就放在 _NSConcreteStackBlock 中鞋既。
Block 截獲變量
如下面例子力九,定義一個(gè) Block耍铜,在代碼塊里打印變量 a,然后修改后調(diào)用 Block 打印出的變量 a 的值不變跌前。
NSInteger val = 10;
void (^blockName)(void) = ^{
NSLog(@"val = %ld", val);
};
blockName(); // val = 10
val = 20;
blockName(); // val = 10
為何修改后打印 a 的值不變棕兼,因?yàn)?Block 語(yǔ)法的表達(dá)式使用的是它之前聲明的局部變量 a。Block 表達(dá)式截獲所使用的局部變量的值抵乓,保存了該變量的瞬時(shí)值程储。所以在第二次執(zhí)行 Block 表達(dá)式時(shí),即使已經(jīng)改變了局部變量 a 的值臂寝,也不會(huì)影響 Block 表達(dá)式在執(zhí)行時(shí)所保存的局部變量的瞬時(shí)值章鲤。
這就是 Block 變量截獲局部變量值的特性。
通過(guò)上面的方法轉(zhuǎn)換為 C語(yǔ)言咆贬,可以看到在 Block 中使用外面的變量败徊,變量被作為成員變量追加到 __main_block_impl_0 結(jié)構(gòu)體中了。
從上圖中可看到掏缎,截獲自動(dòng)變量皱蹦,就是在表達(dá)式中所使用的變量被保存到 Block 的結(jié)構(gòu)體中 __cself->val,所以在 Block 之外修改變量 val 的值眷蜈,Block 表達(dá)式里面的 val 值不變沪哺。
另外,在使用 C語(yǔ)言數(shù)組時(shí)要注意酌儒,截獲自動(dòng)變量的方法沒(méi)有實(shí)現(xiàn)對(duì) C 數(shù)組的截獲辜妓,可使用指針來(lái)解決。
如上圖所示忌怎,為何會(huì)報(bào)錯(cuò)呢籍滴?
通過(guò)上面截獲變量的例子可知,變量被賦值給 Block 結(jié)構(gòu)體中的成員變量榴啸,因?yàn)?C語(yǔ)言數(shù)組類(lèi)型變量不能賦值給數(shù)組類(lèi)型變量:char a[10]={'a'}; char b[10]=a; 這樣編譯不能通過(guò)孽惰,所以會(huì)報(bào)錯(cuò)。
__block 說(shuō)明符
在 Block 中只能使用保存的局部變量的瞬時(shí)值鸥印,并不能直接對(duì)其進(jìn)行修改勋功,想要修改需要在局部變量前加 __block 修飾。
__block NSInteger val = 10;
void (^blockName)(void) = ^{
val = 30;
NSLog(@"val = %ld", val);
};
blockName(); // val = 30
__block 類(lèi)似于 static库说、auto狂鞋,用于指定將變量值設(shè)置到哪個(gè)存儲(chǔ)域中。auto 表示作為自動(dòng)變量存儲(chǔ)在棧中璃弄,static 表示作為靜態(tài)變量存儲(chǔ)在數(shù)據(jù)區(qū)中要销。
繼續(xù)轉(zhuǎn)換為 C語(yǔ)言來(lái)看一下,如下圖:
可看到夏块,使用在變量前加上 __block 說(shuō)明符后疏咐,代碼增加了很多纤掸,__block 變量變成了__Block_byref_val_0 結(jié)構(gòu)體類(lèi)型的變量。
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
NSInteger val;
};
__Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 30;
其中結(jié)構(gòu)體的成員變量 *__Block_byref_val_0 __forwarding 是指向自身的指針浑塞,通過(guò)這個(gè)指針來(lái)訪問(wèn)變量 __forwarding->val借跪。為何這樣使用,下面來(lái)介紹酌壕。
Block 存儲(chǔ)域
前面說(shuō)過(guò)掏愁,Block 是作為 OC 對(duì)象處理的,impl.isa = &_NSConcreteStackBlock; 該類(lèi)的信息就放在 _NSConcreteStackBlock 中卵牍。
除了 _NSConcreteStackBlock 類(lèi)型果港,還有其他兩種類(lèi)型 _NSConcreteGlobalBlock 和 _NSConcreteMallocBlock,這三類(lèi)所在內(nèi)存存儲(chǔ)區(qū)域有區(qū)別:
- _NSConcreteStackBlock 棧區(qū)
- _NSConcreteGlobalBlock 數(shù)據(jù)區(qū)域
- _NSConcreteMallocBlock 堆區(qū)
_NSConcreteGlobalBlock
前面出現(xiàn)的 Block 例子糊昙,使用的是 _NSConcreteStackBlock 設(shè)置在棧上辛掠。
在全局變量使用 Block 時(shí),生成的是 _NSConcreteGlobalBlock 類(lèi)對(duì)象释牺,如下:
void (^blockName)(void) = ^{ NSLog(@"block"); };
int main() {
blockName();
return 0;
}
因?yàn)樵谑褂萌肿兞康牡胤讲荒苁褂米詣?dòng)變量萝衩,所以就不存在堆自動(dòng)變量進(jìn)行截獲。此種類(lèi)型的 Block 結(jié)構(gòu)體實(shí)例內(nèi)容不依賴(lài)于執(zhí)行的狀態(tài)没咙,所以整個(gè)程序只需一個(gè)實(shí)例猩谊,將 Block 用結(jié)構(gòu)體實(shí)例設(shè)置在與全局變量相同的數(shù)據(jù)區(qū)域中即可。
總結(jié)來(lái)說(shuō)就是祭刚,在全局變量有 Block 語(yǔ)法時(shí)牌捷,Block 語(yǔ)法的表達(dá)式不使用截獲的自動(dòng)變量時(shí),Block 為 _NSConcreteGlobalBlock 類(lèi)對(duì)象袁梗。
_NSConcreteMallocBlock
配置在全局變量上的 Block 在變量作用域外也可以通過(guò)指針訪問(wèn)使用宜鸯,但是配置在棧上的 Block 若其所在變量作用域結(jié)束,該 Block 就被廢棄遮怜,同樣的 __block 變量也配置在棧上,若所在變量作用域結(jié)束鸿市,則該 __block 變量也會(huì)被廢棄锯梁。
因此提供了將 Block 和 __block 變量從棧上復(fù)制到堆上的方法來(lái)解決這個(gè)問(wèn)題,從棧上復(fù)制到堆上焰情,即使變量作用域結(jié)束陌凳,堆上的 Block 還可以繼續(xù)存在。
復(fù)制到堆上的 Block 將 _NSConcreteMallocBlock 類(lèi)對(duì)象寫(xiě)入到 Block 結(jié)構(gòu)體實(shí)例的成員變量 isa: impl.isa = &_NSConcreteMallocBlock;
__block 變量的結(jié)構(gòu)體成員變量 __forwarding 可以實(shí)現(xiàn)無(wú)論 __block 變量配置在棧上内舟,還是堆上都能正確的訪問(wèn) __block 變量合敦。
在 ARC 模式下,編譯器會(huì)判斷验游,自動(dòng)生成將 Block 從棧上復(fù)制到堆上的代碼充岛。但是當(dāng)保檐,向方法或函數(shù)的參數(shù)中傳遞 Block 時(shí),需要手動(dòng)復(fù)制崔梗,如下面的代碼夜只。
- (NSArray *)getBlockArray {
int val = 10;
//需要使用 copy 復(fù)制,不然函數(shù)執(zhí)行完成蒜魄,棧上的 Block 被廢棄扔亥,執(zhí)行報(bào)錯(cuò)
return [NSArray arrayWithObjects:
[^{NSLog(@"blk0: %d", val);} copy],
[^{NSLog(@"blk1: %d", val);} copy] , nil];
}
NSArray *tempArray = [self getBlockArray];
void(^blk)(void) = [tempArray objectAtIndex:0];
blk();
如果在方法或函數(shù)中復(fù)制了傳遞過(guò)來(lái)的參數(shù),那么就不必再調(diào)用該方法或函數(shù)前手動(dòng)復(fù)制了谈为,例如旅挤,在方法命中含有 usingBlock 時(shí),[array enumerateObjectsUsingBlock:...]伞鲫,或者 GCD 的 API 中粘茄,不用手動(dòng)復(fù)制。
當(dāng)對(duì) Block 調(diào)用 copy 方法時(shí)榔昔,_NSConcreteStackBlock 類(lèi)會(huì)從棧復(fù)制到堆上驹闰。_NSConcreteGlobalBlock 類(lèi)什么也不做。_NSConcreteMallocBlock 類(lèi)引用計(jì)數(shù)器增加撒会。
什么時(shí)候棧上的 Block 會(huì)復(fù)制到堆上:
- 調(diào)用 Block 的 copy 實(shí)例方法時(shí)
- Block 作為函數(shù)返回值返回時(shí)
- 將 Block 賦值給有 __strong 修飾符 id 類(lèi)型的類(lèi)或 Block類(lèi)型變量時(shí)
- 在方法名含有 usingBlock 的 Coca 框架方法 或 GCD 的 API 中傳遞 Block 時(shí)
注意下面幾種情況嘹朗,ARC下:
//將 Block 賦值給有 __strong 修飾符 id 類(lèi)型的類(lèi)或 Block類(lèi)型變量時(shí),棧上的 Block 會(huì)復(fù)制到堆上
NSInteger val = 3;
NSLog(@"block = %@", ^{ NSLog(@"val = %ld", val); });
//block = <__NSStackBlock__: 0x7ffeefbff540>
NSInteger i = 6;
//捕獲變量诵肛,和上面的對(duì)比屹培,這里將 Block 賦值,會(huì)從棧上復(fù)制到堆上
void (^blockName)(void) = ^{ NSLog(@"i = %ld", i); };
blockName(); // i = 6
NSLog(@"block = %@", blockName);
//block = <__NSMallocBlock__: 0x10280df70>
__block NSInteger val = 3;
NSLog(@"block = %@", ^{ val = 30; NSLog(@"val = %ld", val); });
//block = <__NSStackBlock__: 0x7ffeefbff528>
__block NSInteger i = 6;
void (^blockName)(void) = ^{ i = 60; NSLog(@"i = %ld", i); };
blockName(); // i = 60
NSLog(@"block = %@", blockName);
//block = <__NSMallocBlock__: 0x100704250>
//在沒(méi)有捕獲自動(dòng)變量時(shí)怔檩, Block 結(jié)構(gòu)體實(shí)例是 _NSConcreteGlobalBlock
NSLog(@"block = %@", ^{ NSLog(@"no val"); });
//block = <__NSGlobalBlock__: 0x100001028>
static NSInteger val = 3;
NSLog(@"block = %@", ^{ val = 30; NSLog(@"val = %ld", val); });
//block = <__NSGlobalBlock__: 0x100001028>
void (^blockName)(void) = ^{ NSLog(@"no val"); };
blockName();
NSLog(@"block = %@", blockName);
//block = <__NSGlobalBlock__: 0x100001048>
static NSInteger i = 6;
void (^blockName)(void) = ^{ i = 60; NSLog(@"i = %ld", i); };
blockName(); // i = 60
NSLog(@"block = %@", blockName); //block = <__NSGlobalBlock__: 0x100001048>
__block 變量和對(duì)象
當(dāng) Block 從棧復(fù)制到堆時(shí)褪秀, __block 變量也全部被從棧復(fù)制到堆并被 Block 所持有。
若有多個(gè) Block 使用同一個(gè) __block 變量時(shí)薛训,任何一個(gè) Block 從棧復(fù)制到堆時(shí)媒吗,__block 變量也會(huì)從棧復(fù)制到堆,剩下的 Block 從棧復(fù)制到堆乙埃,被復(fù)制的 Block 持有 __block 變量并增加 __block 變量的引用計(jì)數(shù)闸英。
若配置在堆上的 Block 被廢棄,它所使用的 __block 變量也會(huì)被釋放介袜。
前面提到 __block 變量的結(jié)構(gòu)體成員變量 __forwarding 可以實(shí)現(xiàn)無(wú)論 __block 變量配置在棧上甫何,還是堆上都能正確的訪問(wèn) __block 變量。如下例子:
__block NSInteger val = 10;
void (^blockName)(void) = [^{val = 30; NSLog(@"val = %ld", val); } copy];
blockName(); // val = 30
NSLog(@" %ld ", val); //30
val = 20;
NSLog(@" %ld ", val); //20
__block 變量從棧上復(fù)制到堆上遇伞,此時(shí)會(huì)將成員變量 __forwarding 的值替換為復(fù)制到堆上的 __block 變量結(jié)構(gòu)體實(shí)例的地址辙喂。val.__forwarding 使用的是同一個(gè)在堆上的值,從而能保證正確的訪問(wèn)同一個(gè) __block 變量。
上圖中有兩個(gè)函數(shù)巍耗,__main_block_copy_0 和 __main_block_dispose_0秋麸,在 Block 復(fù)制到堆上和從堆上釋放時(shí)被調(diào)用。將使用到的 __block 變量或者截獲的對(duì)象復(fù)制給 Block 結(jié)構(gòu)體的成員變量芍锦,持有對(duì)象竹勉。這樣截獲的對(duì)象就能夠超出其變量作用域而存在。通過(guò)參數(shù) BLOCK_FIELD_IS_BYREF 和 BLOCK_FIELD_IS_OBJECT 來(lái)區(qū)分函數(shù)對(duì)象類(lèi)型是 __block 變量還是對(duì)象娄琉。
id array = [NSMutableArray array];
void (^blk)(void) = ^(){
//截獲對(duì)象
[array addObject:@(1)];
NSLog(@"Block ! %ld", [array count]);
};
blk();
Block 的循環(huán)引用問(wèn)題
如在 block 中使用了對(duì)象次乓, block 會(huì)對(duì)使用的對(duì)象進(jìn)行持有,如該對(duì)象同時(shí)持有該 block 則會(huì)造成循環(huán)引用的問(wèn)題孽水,互相持有不能釋放票腰。
self.blockName = ^() {
[self.delegate doSomething];
};
//解決方法,在 ARC 下使用 __weak 進(jìn)行修飾女气。這樣 block 就不持有 self杏慰,避免循環(huán)引用。
__weak __typeof(self) weakSelf = self;
self.blockName = ^() {
[weakSelf.delegate doSomething];
};
//下面代碼會(huì)有內(nèi)存泄漏嗎
- (void)viewDidLoad {
[super viewDidLoad];
NSNotificationCenter *__weak center = [NSNotificationCenter defaultCenter];
id __block token = [center addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * _Nonnull note) {
[self doSomething];
[center removeObserver:token];
token = nil;
}];
}
- (void)doSomething {
}
- (void)dealloc {
NSLog(@"%s", __FUNCTION__);
}
//__block token炼鞠,如果不加 __block 在Block里的 [center removeObserver:token]; token 為空缘滥。
//因?yàn)?token 在執(zhí)行完后才返回值,所以一開(kāi)始捕獲到的谒主,是返回之前的沒(méi)有被初始化的朝扼。
//加上 __block 是通過(guò)指針 __forwarding->token 取值卖漫,能夠正確訪問(wèn)到骡技。
//上面如果 block 沒(méi)有執(zhí)行,則會(huì)內(nèi)存泄漏蔓钟。center 持有 token观游,token 持有 block搂捧,block 持有 self 也持有 token,
//token 不釋放懂缕,self 不會(huì)釋放
//最簡(jiǎn)單的解決方法就是
__weak typeof(self) wkSelf = self;
id __block __weak wkToken = [wkCenter addObserverForName:UIApplicationDidEnterBackgroundNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification * _Nonnull note) {
[wkSelf doSomething];
[wkCenter removeObserver:wkToken];
}];
__weak允跑、 __strong 的使用
聲明一個(gè)對(duì)象:
id __strong obj = [[NSObject alloc] init];
//編譯器會(huì)轉(zhuǎn)換為下面的代碼
id __attribute__((objc_ownership(strong))) obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
//上面的代碼其實(shí)就是下面的函數(shù)調(diào)用
id obj = objc_msgSend(NSObject, @selector(alloc));
objc_msgSend(obj,selector(init));
objc_release(obj);
__weak id weakObj = obj;
//轉(zhuǎn)換為
__attribute__((objc_ownership(weak))) id weakObj = obj;
//相關(guān)的調(diào)用
id weakObj ;
objc_initWeak(&weakObj,obj);
objc_destoryWeak(&weakObj);
id objc_initWeak(id *object, id value) {
*object = nil;
return objc_storeWeak(object, value);
}
void objc_destroyWeak(id *object) {
objc_storeWeak(object, nil);
}
有關(guān)底層實(shí)現(xiàn)可查看 clang 文檔 http://clang.llvm.org/docs/AutomaticReferenceCounting.html
weak 表是用Hash table實(shí)現(xiàn)的, objc_storeWeak 函數(shù)就把第一個(gè)入?yún)⒌淖兞康刂纷?cè)到weak表中搪柑,然后根據(jù)第二個(gè)入?yún)?lái)決定是否移除吮蛹。如果第二個(gè)參數(shù)為0,那么就把 __weak變量從weak表中刪除記錄拌屏,并從引用計(jì)數(shù)表中刪除對(duì)應(yīng)的鍵值記錄。
如果 __weak 引用的原對(duì)象如果被釋放了术荤,那么對(duì)應(yīng)的 __weak 對(duì)象就會(huì)被指為nil倚喂。就是通過(guò) objc_storeWeak 函數(shù)這些函數(shù)來(lái)實(shí)現(xiàn)的。
我們已經(jīng)知道使用 weakSelf 來(lái)解決循環(huán)引用的問(wèn)題,為何有的還需要在 block 里使用 strongSelf ?
__weak __typeof(self) weakSelf = self;
self.blockName = ^() {
__strong typeof(weakSelf) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[strongSelf.delegate doSomething];
});
};
有的情況下 block 會(huì)延遲調(diào)用端圈,在 block 未調(diào)用前 self 可能 已經(jīng)釋放掉了焦读,這時(shí)再在 block 使用 weakSelf ,weakSelf 為空了舱权。在 block 里面使用的 __strong 修飾的 weakSelf 是為了在函數(shù)生命周期中防止 self 提前釋放矗晃。strongSelf 是一個(gè)自動(dòng)變量當(dāng) block 執(zhí)行完畢就會(huì)釋放自動(dòng)變量 strongSelf ,不會(huì)對(duì) self 進(jìn)行一直進(jìn)行強(qiáng)引用宴倍。
總結(jié)來(lái)說(shuō)就是张症,weakSelf 是為了 block 不持有 self,避免循環(huán)引用鸵贬。strongSelf 防止 self 提前釋放俗他。