上一篇 文章 簡(jiǎn)要解釋了Block的基本概念秃诵,打一些基礎(chǔ),從而能夠更好的了解關(guān)于編程語(yǔ)言中類似于Objective-C Block這種塊級(jí)邏輯單元的語(yǔ)法本質(zhì)蛀缝,其實(shí)在編程語(yǔ)言通用的概念中,類似于Block這種的語(yǔ)法被稱為lambda表達(dá)式目代,也被稱為匿名函數(shù)屈梁,也就是不需要定義函數(shù)名的函數(shù)或者是子程序,在很多現(xiàn)代化的高級(jí)語(yǔ)言中都普遍存在榛了,比方說(shuō)Java8中通過(guò)箭頭函數(shù)來(lái)實(shí)現(xiàn)lambda表達(dá)式在讶,消除了以前被廣為詬病的通過(guò)接口實(shí)例化內(nèi)部類來(lái)實(shí)現(xiàn)類似于lambda的handler。JavaScript因?yàn)楹瘮?shù)本身就是一等公民霜大,可以進(jìn)行傳遞构哺,自然也就支持匿名的函數(shù)作為參數(shù),作為變量战坤。Python支持lambda關(guān)鍵字曙强,實(shí)現(xiàn)匿名參數(shù),非常簡(jiǎn)單途茫,但只能實(shí)現(xiàn)一些簡(jiǎn)單的函數(shù)碟嘴。C++、C#也都實(shí)現(xiàn)了lambda表達(dá)式囊卜,Swift娜扇、Ruby則強(qiáng)調(diào)閉包的概念,本質(zhì)上與lambda表達(dá)式是同樣一種東西栅组。
那對(duì)于Block來(lái)說(shuō)雀瓢,我們要抓住其核心本質(zhì)的東西,才能避免各類語(yǔ)言在這方面的技術(shù)陷阱玉掸,才能觸類旁通刃麸,提高效率。
為什么在@property(nonatomic,copy)void(^block)(void)聲明Block變量使用copy司浪?
在編寫日常業(yè)務(wù)代碼時(shí)嫌蚤,一種常用的范式就是異步執(zhí)行任務(wù)辐益,或者說(shuō)是handler回調(diào)。這種需求往往可以通過(guò)調(diào)用方設(shè)置block從而代替繁瑣的delegate模式脱吱,但被調(diào)用方一般都需要持有這個(gè)block智政,則經(jīng)常會(huì)聲明一個(gè)block的屬性,而一般@property中則經(jīng)常使用copy關(guān)鍵字箱蝠,我們來(lái)看一下具體的原因续捂。
上一篇我們提到有一種block是棧Block,在局部聲明的Block就是這種宦搬,隨著作用域的消失而出棧牙瓢,從而消失,那么间校,當(dāng)調(diào)用方聲明一個(gè)block傳入被調(diào)用方進(jìn)行持有的時(shí)候矾克,則需要將棧區(qū)的block拷貝到堆區(qū),所以才使用copy憔足,但是在ARC中寫不寫都行胁附,因?yàn)锳RC會(huì)自動(dòng)copy,從而保證block的安全調(diào)用滓彰。大家都使用copy來(lái)聲明控妻,則是一種習(xí)慣,當(dāng)然這種習(xí)慣是非常好的揭绑,至少弓候,如果被調(diào)用方不聲明copy,調(diào)用方有可能會(huì)自己再調(diào)用copy從而多此一舉他匪。官方的詳細(xì)原因解釋如下:
Objects Use Properties to Keep Track of Blocks
Block的值捕獲
對(duì)于函數(shù)與方法菇存,最明顯的區(qū)別莫過(guò)于函數(shù)是無(wú)狀態(tài)的,而方法是有上下文依賴的邦蜜,而這個(gè)上下文則是由對(duì)象內(nèi)部的狀態(tài)和數(shù)據(jù)組合提供的撰筷。Block或者是lambda表達(dá)式,之所以能夠使用到諸如異步執(zhí)行任務(wù)或者說(shuō)是回調(diào)中畦徘,原因就在于它方便的能夠引用上下文的內(nèi)容毕籽,而這個(gè)上下文則是Block聲明的地方所在的上下文,這也是這種塊級(jí)語(yǔ)法相對(duì)于其他語(yǔ)法比較難以掌握的地方井辆,而如何引用上下文的內(nèi)容关筒,這個(gè)話題被稱為作用域的值捕獲問(wèn)題(scope capture value)。
如果Block沒(méi)有引用上下文以外的任何東西杯缺,那么這個(gè)問(wèn)題也就不用討論蒸播,那么單純考慮引用上下文內(nèi)容的情況,可以劃分為兩類情況:
- 只引用不修改
- 既引用又修改
只引用,不修改:
對(duì)于在block內(nèi)部引用上下文當(dāng)中的變量袍榆,可以分為值類型和引用類型胀屿,值類型就是,不管怎樣在上下文中傳遞包雀,只傳遞該變量的值宿崭,比如int、char等這些基本數(shù)據(jù)類型才写,引用類型其實(shí)指的就是指針葡兑,這種不傳遞變量實(shí)際的值,而只傳遞指向該變量?jī)?nèi)存地址的指針赞草,其實(shí)指針的內(nèi)容本質(zhì)上也是基本數(shù)據(jù)類型讹堤,屬于值類型,對(duì)于值類型厨疙,block對(duì)其引用就是直接將該變量的值給復(fù)制一遍到block內(nèi)部洲守,方便以后使用。例子如下:
-(void)whatisblock{
int count = 0;
void(^block)(void);
NSString * testString = @"test block";
block = ^(){
NSLog(@"%@ %d", testString, count);
};
block();
}
block中捕獲了count變量與testString變量沾凄,一個(gè)是int類型的值類型梗醇,一個(gè)是指向NSString類型的指針類型,也就是引用類型搭独。通過(guò)clang 編譯成C++代碼以后婴削,可以發(fā)現(xiàn):
//whatisblock方法的實(shí)現(xiàn)
static void _I_TestBlock_whatisblock(TestBlock * self, SEL _cmd) {
int count = 0;//局部變量廊镜,值類型
void(*block)(void);
NSString * testString = (NSString *)&__NSConstantStringImpl__var_folders_rk_qz09dkwd485dfckp9sxb63ph0000gn_T_TestBlock_c39702_mi_1;//局部變量牙肝,引用類型,但是@"test block"是個(gè)常量嗤朴,所以實(shí)質(zhì)上是一個(gè)靜態(tài)變量
block = ((void (*)())&__TestBlock__whatisblock_block_impl_0((void *)__TestBlock__whatisblock_block_func_0, &__TestBlock__whatisblock_block_desc_0_DATA, testString, count, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
NSNumber * num = ((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 0);
void(*stackBlock)(void) =((void (*)())&__TestBlock__whatisblock_block_impl_1((void *)__TestBlock__whatisblock_block_func_1, &__TestBlock__whatisblock_block_desc_1_DATA, num, 570425344));
((void (*)(__block_impl *))((__block_impl *)stackBlock)->FuncPtr)((__block_impl *)stackBlock);
Class blockClass = object_getClass((id)stackBlock);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_rk_qz09dkwd485dfckp9sxb63ph0000gn_T_TestBlock_c39702_mi_4,NSStringFromClass(blockClass));
}
struct __TestBlock__whatisblock_block_impl_0 {
struct __block_impl impl;
struct __TestBlock__whatisblock_block_desc_0* Desc;
NSString *testString;//直接引用指針的值
int count;//直接復(fù)制值類型的值
__TestBlock__whatisblock_block_impl_0(void *fp, struct __TestBlock__whatisblock_block_desc_0 *desc, NSString *_testString, int _count, int flags=0) : testString(_testString), count(_count) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
從__TestBlock__whatisblock_block_impl_0
block的結(jié)構(gòu)體中發(fā)現(xiàn)配椭,block捕獲的上下文的變量都會(huì)在結(jié)構(gòu)體中聲明為成員變量,在初始化block的時(shí)候雹姊,將值類型的count變量直接復(fù)制到自己的結(jié)構(gòu)體中股缸,將引用的指針的值也直接復(fù)制到自己的結(jié)構(gòu)體中,但是通過(guò)指針引用捕獲到的引用類型testString吱雏,當(dāng)然敦姻,ARC情況下如果是引用類型,會(huì)直接操作引用計(jì)數(shù)從而保證變量不會(huì)被銷毀從而保證能夠安全訪問(wèn)歧杏。
總結(jié)一下:
- block捕獲的值類型镰惦,會(huì)直接復(fù)制變量的值
- block捕獲的引用類型,會(huì)直接復(fù)制引用的指針的值犬绒,根據(jù)指針的類型旺入,使用ARC進(jìn)行內(nèi)存管理,保證block訪問(wèn)引用類型的引用
- 如果能確定引用類型在block執(zhí)行的生命周期內(nèi)一直存在,則可以使用__weak來(lái)告知ARC不增加引用計(jì)數(shù)茵瘾,破除Block的retain cycle就是基于這個(gè)原理
- 如果捕獲的是全局變量或者是靜態(tài)變量或者是靜態(tài)全局變量礼华,則根據(jù)是否可以安全訪問(wèn)到的原則,相應(yīng)的進(jìn)行指針復(fù)制拗秘,不受ARC影響圣絮,大家可以自己做個(gè)實(shí)驗(yàn),來(lái)驗(yàn)證一下這些類型的變量如何進(jìn)行捕獲的
對(duì)于捕獲變量來(lái)說(shuō)聘殖,諸如block和lambda表達(dá)式要解決的一個(gè)核心問(wèn)題就是如何保證能夠安全的訪問(wèn)到之前捕獲到的變量晨雳,如果是值類型,則直接復(fù)制奸腺,從而保證能夠安全引用餐禁,如果是引用類型,則通過(guò)本門語(yǔ)言的內(nèi)存管理模型來(lái)保證能夠正確的捕獲上下文的變量從而能夠安全訪問(wèn)突照,那么這種技術(shù)的核心要義在于語(yǔ)言對(duì)作用域(scope)的上下文捕獲原則和內(nèi)存管理模式帮非。
既引用,又修改
Block引用的方式已經(jīng)非常清楚讹蘑,那么對(duì)于修改則需要著重研究一下:
//warning: Variable is not assignable (missing __block type specifier)
int count = 0;
void(^block)(void);
block = ^(){
count = 10;
NSLog(@"%d", count);
};
這種寫法會(huì)導(dǎo)致編譯器報(bào)錯(cuò)末盔,提示為變量不是可被引用的,因?yàn)閏ount的作用域在當(dāng)前方法體內(nèi)座慰,變量在棧內(nèi)陨舱,隨著方法調(diào)用結(jié)束,出棧變量銷毀版仔,則block內(nèi)無(wú)法引用修改游盲,那么根據(jù)提示需要在需要修改的變量之前加上__block.
//warning: Variable is not assignable (missing __block type specifier)
__block int count = 0;
void(^block)(void);
block = ^(){
count = 10;
NSLog(@"%@ %d", testString, count);
};
再編譯一下源碼,看一下
struct __TestBlock__whatisblock_block_impl_0 {
struct __block_impl impl;
struct __TestBlock__whatisblock_block_desc_0* Desc;
__Block_byref_count_0 *count; // by ref
__TestBlock__whatisblock_block_impl_0(void *fp, struct __TestBlock__whatisblock_block_desc_0 *desc, __Block_byref_count_0 *_count, int flags=0) : count(_count->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __TestBlock__whatisblock_block_func_0(struct __TestBlock__whatisblock_block_impl_0 *__cself) {
__Block_byref_count_0 *count = __cself->count; // bound by ref
(count->__forwarding->count) = 10;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_rk_qz09dkwd485dfckp9sxb63ph0000gn_T_TestBlock_7b0cc2_mi_1, (count->__forwarding->count));
}
// @implementation TestBlock
struct __Block_byref_count_0 {
void *__isa;
__Block_byref_count_0 *__forwarding;
int __flags;
int __size;
int count;
};
static void _I_TestBlock_whatisblock(TestBlock * self, SEL _cmd) {
__attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,(__Block_byref_count_0 *)&count, 0, sizeof(__Block_byref_count_0), 0};
void(*block)(void);
block = ((void (*)())&__TestBlock__whatisblock_block_impl_0((void *)__TestBlock__whatisblock_block_func_0, &__TestBlock__whatisblock_block_desc_0_DATA, (__Block_byref_count_0 *)&count, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
與單純捕獲相比蛮粮,如果修改了count益缎,則__block會(huì)將變量重寫為一個(gè)結(jié)構(gòu)體,類型為
struct __Block_byref_count_0 {
void *__isa;//isa指針然想,指向類型
__Block_byref_count_0 *__forwarding; //指向自己所在的內(nèi)存地址
int __flags;
int __size;
int count;//原值
};
為了能夠使方法體內(nèi)聲明的變量不因出棧而銷毀莺奔,則將count轉(zhuǎn)換為結(jié)構(gòu)體,復(fù)制到堆上变泄,在static void __TestBlock__whatisblock_block_func_0
函數(shù)體內(nèi)炉旷,則通過(guò)count->__forwarding->count
來(lái)訪問(wèn)称诗。這么做是因?yàn)樵跅I系腸ount的結(jié)構(gòu)體變量,其__forwarding指向被復(fù)制到堆上的count內(nèi)存,而在堆上的count變量的__forwarding又指向自身捏顺,所以通過(guò)count->__forwarding->count
訪問(wèn)能一直訪問(wèn)堆上的count變量跃闹。
從以上的分析來(lái)看這樣一個(gè)規(guī)律次乓,如果要修改捕獲的上下文的變量疆液,則需要通過(guò)重新構(gòu)建引用變量的結(jié)構(gòu)體類型,通過(guò)ARC復(fù)制到堆上,修改完引用計(jì)數(shù)以后就可以安全訪問(wèn)典予,但是如果出現(xiàn)相互引用的情況甜滨,則因?yàn)锳RC在復(fù)制到堆的情況下會(huì)增加引用計(jì)數(shù),就會(huì)出現(xiàn)retain環(huán)的問(wèn)題瘤袖,所以衣摩,如果是相互持有,被引用的變量如果在block的生命周期內(nèi)一直存在捂敌,則可以通過(guò)__weak來(lái)取消ARC的引用計(jì)數(shù)操作艾扮,這樣也是能夠保證被捕獲的變量的安全的。
最終占婉,通過(guò)了解值類型和引用類型在block作用域內(nèi)的引用方式泡嘴,再通過(guò)研究如果修改局部變量引用方式如何改變,block內(nèi)的引用和修改逆济,可以更加清楚的通過(guò)ARC來(lái)解釋這些現(xiàn)象酌予。
Block的內(nèi)存管理
本文一直討論的都是ARC的情況,在MRC的情況下奖慌,上述討論的結(jié)果又極大的不同抛虫,因?yàn)镸RC的方式是完全C的內(nèi)存管理方式,需要主動(dòng)調(diào)用copy或者是棧與堆的內(nèi)存拷貝简僧,而在ARC情況下建椰,只需要記憶Block捕獲的變量為了能夠在Block的生命周期內(nèi)安全引用,ARC會(huì)自動(dòng)copy和引用計(jì)數(shù)提升來(lái)達(dá)到持有變量岛马,當(dāng)然需要注意的就是ARC接管以后出現(xiàn)的Block循環(huán)引用問(wèn)題棉姐。
總結(jié):
- Block在Objective-C中的實(shí)現(xiàn),依賴于其內(nèi)存管理方式蛛枚,使得block能夠捕獲的上下文變量能夠被安全引用谅海,但也要注意單一的引用計(jì)數(shù)可能引起的相互引用問(wèn)題
- 在Swift中脸哀,閉包也是如此蹦浦,閉包能夠捕獲的上下文變量和內(nèi)存管理方式基本上也是ARC的常規(guī)表現(xiàn),所以可以通過(guò)弱引用或者無(wú)主引用破除循環(huán)引用
- 在使用GC(垃圾回收)的語(yǔ)言中撞蜂,lambda表達(dá)式不存在循環(huán)引用的問(wèn)題盲镶,因?yàn)槌艘糜?jì)數(shù),GC還有標(biāo)記蝌诡、分代溉贿、計(jì)劃清理、引用更新等來(lái)消除循環(huán)引用浦旱,例如Python和C#都不存在像Block這樣的問(wèn)題