來(lái)聊聊Block(二)

上一篇 文章 簡(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)容的情況,可以劃分為兩類情況:

  1. 只引用不修改
  2. 既引用又修改

只引用,不修改:

對(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_0block的結(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é)一下:

  1. block捕獲的值類型镰惦,會(huì)直接復(fù)制變量的值
  2. block捕獲的引用類型,會(huì)直接復(fù)制引用的指針的值犬绒,根據(jù)指針的類型旺入,使用ARC進(jìn)行內(nèi)存管理,保證block訪問(wèn)引用類型的引用
  3. 如果能確定引用類型在block執(zhí)行的生命周期內(nèi)一直存在,則可以使用__weak來(lái)告知ARC不增加引用計(jì)數(shù)茵瘾,破除Block的retain cycle就是基于這個(gè)原理
  4. 如果捕獲的是全局變量或者是靜態(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é):

  1. Block在Objective-C中的實(shí)現(xiàn),依賴于其內(nèi)存管理方式蛛枚,使得block能夠捕獲的上下文變量能夠被安全引用谅海,但也要注意單一的引用計(jì)數(shù)可能引起的相互引用問(wèn)題
  2. 在Swift中脸哀,閉包也是如此蹦浦,閉包能夠捕獲的上下文變量和內(nèi)存管理方式基本上也是ARC的常規(guī)表現(xiàn),所以可以通過(guò)弱引用或者無(wú)主引用破除循環(huán)引用
  3. 在使用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)題
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末宇色,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌宣蠕,老刑警劉巖例隆,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異抢蚀,居然都是意外死亡镀层,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門皿曲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)唱逢,“玉大人,你說(shuō)我怎么就攤上這事屋休∥牍牛” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵劫樟,是天一觀的道長(zhǎng)绸贡。 經(jīng)常有香客問(wèn)我,道長(zhǎng)毅哗,這世上最難降的妖魔是什么听怕? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮虑绵,結(jié)果婚禮上尿瞭,老公的妹妹穿的比我還像新娘。我一直安慰自己翅睛,他們只是感情好声搁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著捕发,像睡著了一般疏旨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上扎酷,一...
    開(kāi)封第一講書(shū)人閱讀 51,301評(píng)論 1 301
  • 那天檐涝,我揣著相機(jī)與錄音,去河邊找鬼法挨。 笑死谁榜,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的凡纳。 我是一名探鬼主播窃植,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼荐糜!你這毒婦竟也來(lái)了巷怜?” 一聲冷哼從身側(cè)響起葛超,我...
    開(kāi)封第一講書(shū)人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎延塑,沒(méi)想到半個(gè)月后巩掺,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡页畦,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年胖替,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片豫缨。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡独令,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出好芭,到底是詐尸還是另有隱情燃箭,我是刑警寧澤,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布舍败,位于F島的核電站招狸,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏邻薯。R本人自食惡果不足惜裙戏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望厕诡。 院中可真熱鬧累榜,春花似錦、人聲如沸灵嫌。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)寿羞。三九已至猖凛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間绪穆,已是汗流浹背辨泳。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留霞幅,地道東北人漠吻。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓量瓜,卻偏偏與公主長(zhǎng)得像司恳,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子绍傲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 《Objective-C高級(jí)編程》這本書(shū)就講了三個(gè)東西:自動(dòng)引用計(jì)數(shù)扔傅、block耍共、GCD,偏向于從原理上對(duì)這些內(nèi)容...
    WeiHing閱讀 9,810評(píng)論 10 69
  • 一猎塞、Objective-C發(fā)展史 Objective-C從1983年誕生试读,已經(jīng)走過(guò)了30多年的歷程。隨著時(shí)間的推移...
    沒(méi)事蹦蹦閱讀 5,838評(píng)論 12 34
  • 前言 Blocks是C語(yǔ)言的擴(kuò)充功能荠耽,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這...
    小人不才閱讀 3,768評(píng)論 0 23
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理钩骇,服務(wù)發(fā)現(xiàn),斷路器铝量,智...
    卡卡羅2017閱讀 134,654評(píng)論 18 139
  • importUIKit classViewController:UITabBarController{ enumD...
    明哥_Young閱讀 3,805評(píng)論 1 10