來聊聊Block(一)


作為iOS-Advanced第一篇,我們來聊聊Block,這是一個Objective-C語言里開發(fā)者非常喜歡的語法,從這里講起,而不是直接從Swift某個話題開始或者從某個比較復(fù)雜的技術(shù)框架說起,是因?yàn)樽鳛橐粋€iOS進(jìn)階的開發(fā)者,在現(xiàn)階段,必然是各方面技術(shù)水平均衡發(fā)展、深入理解的一個階段,很多人投入Swift、很多人投入混合開發(fā)、很多人投入前端也有很多人投入后端,其實(shí)無論做哪一塊,這一階段都不僅僅是只了解術(shù),而是要了解道的問題,比方說我們把Block在Objective-C中的本質(zhì)和核心問題討論清楚,再聯(lián)想起Swift的閉包,再舉一反三去推廣到其他語言中的lambda表達(dá)式、匿名函數(shù)等概念,那么我們僅僅通過扎實(shí)Objective-C的知識,就能舉一反三提高對Swift、其他編程語言或其他技術(shù)的理解能力,也提升了技術(shù)視野和學(xué)習(xí)能力,這是我們追逐的目標(biāo)。此時,還要把注意力和精力從以往的具體而微的地方轉(zhuǎn)移。

Block的歷史

Block出現(xiàn)在OS X v10.6 和 iOS 4.0 以后,并且在GCC與Clang中都實(shí)現(xiàn)了,作為一種塊級語法補(bǔ)充,Block在C、C++、Objective-C中都能夠良好的使用,該語法是編譯器實(shí)現(xiàn)的,順帶提一下蘋果的編譯器從最早的GCC到后來的LLVM-GCC再到后來的LLVM Compiler(Clang與LLVM),是一段發(fā)展比較痛苦的歷史,為什么說Block是GCC與Clang實(shí)現(xiàn)的呢?我們都知道編譯器的大概工作原理是將代碼文件進(jìn)行編譯、鏈接打包成二進(jìn)制可執(zhí)行程序,而現(xiàn)代化的編譯器則分工更加精細(xì),甚至編譯工具都早早進(jìn)行了模塊化,GCC與Clang都是和LLVM配合使用的,兩者的關(guān)系就像前后臺的關(guān)系,GCC與Clang這種前端編譯器負(fù)責(zé)預(yù)處理 (Preprocess),語法 (lex),解析 (parse),語義分析 (Semantic Analysis),抽象語法樹生成 (Abstract Syntax Tree)等等,LLVM是 Low Level Virtual Machine 的簡稱,這個庫提供了與編譯器相關(guān)的支持,能夠進(jìn)行程序語言的編譯期優(yōu)化、鏈接優(yōu)化、在線編譯優(yōu)化、代碼生成。簡而言之,可以作為多種語言編譯器的后臺來使用。也就說Clang和GCC將Objective-C的源碼通過內(nèi)置的語法分析器編譯成LLVM所需要的結(jié)果再生成匯編最后產(chǎn)生可執(zhí)行的二進(jìn)制文件,那么也就是說Block的語法支持,是給GCC與Clang進(jìn)行擴(kuò)展升級,支持代碼塊這種語法就可以了。

Block究竟是什么?


上面為Block的使用圖解,來自官方文檔 Blocks Programming,我們經(jīng)常使用的Block究竟是什么呢?對于編程語言來講,語法元素其實(shí)不太輕易擴(kuò)充,那Block具有能夠執(zhí)行的能力,封裝了一段代碼邏輯,那它本質(zhì)是什么呢?函數(shù)?方法?對象?一種特殊數(shù)據(jù)類型?Objective-C是對C的擴(kuò)展,根據(jù)Runtime的認(rèn)識,我們知道,Objective-C中的對象是結(jié)構(gòu)體, 方法是對象的結(jié)構(gòu)體里面引用的函數(shù)指針的成員變量,那block究竟是什么東西呢?
編寫一個類代碼如下:

-(void)whatisblock{
    void(^block)(void);
    block = ^(){
        NSLog(@"test block");
    };
}

然后再命令行里使用clang進(jìn)行rewrite:

clang -rewrite-objc TestBlock.m

生成了cpp文件,打開以后我們查看一下whatisblock在哪:

static __NSConstantStringImpl __NSConstantStringImpl__var_folders_rk_qz09dkwd485dfckp9sxb63ph0000gn_T_TestBlock_4778c6_mi_0 __attribute__ ((section ("__DATA, __cfstring"))) = {__CFConstantStringClassReference,0x000007c8,"test block",10};

struct __TestBlock__whatisblock_block_impl_0 {
  struct __block_impl impl;
  struct __TestBlock__whatisblock_block_desc_0* Desc;
  __TestBlock__whatisblock_block_impl_0(void *fp, struct __TestBlock__whatisblock_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;//isa指針
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __TestBlock__whatisblock_block_func_0(struct __TestBlock__whatisblock_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_rk_qz09dkwd485dfckp9sxb63ph0000gn_T_TestBlock_4778c6_mi_0);
    }

static void _I_TestBlock_whatisblock(TestBlock * self, SEL _cmd) {
    void(*block)(void);
//block是一個指向void()(void)類型函數(shù)的函數(shù)指針,賦值語句通過&取地址符強(qiáng)轉(zhuǎn)為函數(shù)指針,__TestBlock__whatisblock_block_impl_0是一個函數(shù),查找定義如上
    block = ((void (*)())&__TestBlock__whatisblock_block_impl_0((void *)__TestBlock__whatisblock_block_func_0, &__TestBlock__whatisblock_block_desc_0_DATA));
}

首先我們找到whatisblock的內(nèi)容:
static void _I_TestBlock_whatisblock(TestBlock * self, SEL _cmd)該函數(shù)是whatisblock方法的實(shí)現(xiàn),里面有一個block變量,正是在源碼方法里的block變量,其類型本質(zhì)上一個結(jié)構(gòu)體,是通過__TestBlock__whatisblock_block_impl_0初始化函數(shù)創(chuàng)建的,里該結(jié)構(gòu)體的成員屬性中有struct __block_impl impl這樣一個成員變量,而impl則有isa指針,大家都是isa指針是什么,那么我們有理由相信, block是個對象 。相關(guān)的__TestBlock__whatisblock_block_func_0是什么?查看一下定義,正是block花括號里面的邏輯實(shí)現(xiàn),函數(shù)指針傳遞給block結(jié)構(gòu)體當(dāng)中的impl.FuncPtr = fp;。
以上,我們能得出兩個結(jié)論:1.block其實(shí)就是一個對象 2.block代碼塊實(shí)際上是一個函數(shù),通過函數(shù)指針傳遞給block對象結(jié)構(gòu)體。
包含的內(nèi)容包含如下圖:


對應(yīng)的block結(jié)構(gòu)體是:

struct __TestBlock__whatisblock_block_impl_0 {
  struct __block_impl impl;
  struct __TestBlock__whatisblock_block_desc_0* Desc;
  __TestBlock__whatisblock_block_impl_0(void *fp, struct __TestBlock__whatisblock_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;//isa指針
    impl.Flags = flags;//標(biāo)識符
    impl.FuncPtr = fp;//block代碼片段的函數(shù)實(shí)現(xiàn)
    Desc = desc;//描述
  }
};

這個結(jié)構(gòu)體是C++的結(jié)構(gòu)體,當(dāng)中與結(jié)構(gòu)體重名的函數(shù)是該結(jié)構(gòu)體的構(gòu)造函數(shù)。struct __block_impl impl;實(shí)際上就是block本身。
__TestBlock__whatisblock_block_desc_0是關(guān)于block本身信息的成員變量,構(gòu)造如下:

static struct __TestBlock__whatisblock_block_desc_0 {
  size_t reserved;//保留大小
  size_t Block_size;//block大小
} __TestBlock__whatisblock_block_desc_0_DATA = { 0, sizeof(struct __TestBlock__whatisblock_block_impl_0)};

很清楚看到,block變量的花括號的代碼片段實(shí)際上轉(zhuǎn)換為一個無狀態(tài)的函數(shù),這個函數(shù)的指針存入自身的Funcptr的變量中,而聲明則為:

static void __TestBlock__whatisblock_block_func_0(struct __TestBlock__whatisblock_block_impl_0 *__cself)

入?yún)⒅挥幸粋€cself,通過這個cself來獲取block引用的變量,如果在block內(nèi)部有引用的變量會在block這個結(jié)構(gòu)體上聲明,這個在第二篇詳細(xì)講述。
block包含三部分:

  1. Block結(jié)構(gòu)體本身
  2. Block對應(yīng)的處理邏輯的函數(shù)
  3. Block引用的變量

Block的類型

從各種文檔中已經(jīng)明確Block的種類:

  1. _NSConcreteGlobalBlock(全局Block)
  2. _NSConcreteStackBlock(棧區(qū)Block)
  3. _NSConcreteMallocBlock(堆區(qū)Block)
    那這里面說的全局、堆區(qū)、堆區(qū)指的是block變量所在的內(nèi)存區(qū)域,從這個角度來分類,從上面的block結(jié)構(gòu)體的內(nèi)容我們知道,block結(jié)構(gòu)體內(nèi)基本上都是保存相關(guān)內(nèi)容的指針,所以這三種block的差別實(shí)際上不僅僅體現(xiàn)在block結(jié)構(gòu)體變量指針?biāo)诘膬?nèi)存區(qū)域,同時變量引用等的區(qū)別也非常大。其實(shí)也應(yīng)該這么說,就是因?yàn)閎lock聲明的方式不同和引用外部變量的類型不同,所以才區(qū)分為這三種block,給block賦予正確的內(nèi)存空間。

_NSConcreteGlobalBlock:
Block變量本身是處于全局區(qū)的,跟全局函數(shù)差不多,能保證調(diào)用時候的安全。
block本身沒有捕獲任何外部變量,或者block是聲明在函數(shù)外部的,則也是全局Block。
如下:

void(^global_block)(void) = ^(){
    NSLog(@"This is global block~");
};

_NSConcreteStackBlock:
Block變量本身在棧區(qū)域,這意味著Block變量是聲明在函數(shù)內(nèi),作為局部變量的,局部變量是在棧中保存,隨著函數(shù)執(zhí)行完而銷毀,在ARC上,發(fā)現(xiàn)函數(shù)或方法當(dāng)中局部聲明的block也是_NSConcreteGlobalBlock類型的。
如以下例子:

-(void)whatisblock{
    void(^block)(void);
    block = ^(){
        NSString * testString = @"test block";
        NSLog(@"%@", testString);
    };
      block();
}

在此,我們發(fā)現(xiàn) clang -rewrite-objc 轉(zhuǎn)換的c++代碼不一定是最后的結(jié)果,因?yàn)闆]有考慮的ARC的情況,比方說轉(zhuǎn)換的C++代碼中,impl.isa = &_NSConcreteStackBlock;//isa指針明明寫的是棧Block,但是運(yùn)行起來斷點(diǎn)查看確是_NSConcreteGlobalBlock類型,說明ARC做過優(yōu)化。


_NSConcreteMallocBlock:
Block變量本身是在堆區(qū)域中的,因?yàn)锽lock需要在聲明的方法或者是函數(shù)結(jié)束以后再執(zhí)行,那么就必須由程序員開辟堆內(nèi)存來保存Block和Block相關(guān)的變量,所以如果默認(rèn)Block是在棧上的,要使用copy操作來將Block復(fù)制到堆區(qū)域中,并且捕獲的相關(guān)變量也必須保留一份在堆內(nèi)存區(qū)域中,這樣才能在調(diào)用的時候能安全使用捕獲的變量。假如一個Block捕獲了外部變量,如果這個外部變量是函數(shù)內(nèi)的局部變量,那么就要將block定義為堆block,這樣才能保存捕獲變量的引用。
如下:

-(void)whatisblock{
    void(^block)(void);
    block = ^(){
        NSString * testString = @"test block";
        NSLog(@"%@", testString);
    };
      block();
      NSNumber * num = @0;
    void(^stackBlock)(void) =^(){
        NSLog(@"This is Malloc Block %@",num);
    };
    stackBlock();
}

以上,就是第一篇關(guān)于Block的討論和分析,下一篇將介紹對于Block比較重要的部分就是scope(作用域)中的變量捕獲和內(nèi)存管理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容