iOS Block用法與實(shí)現(xiàn)原理

  • 最近在看Block原理的時(shí)候看了一篇文章iOS Block用法和實(shí)現(xiàn)原理,并且對(duì)照了《Objective-C高級(jí)編程》這本書中對(duì)于Block的解析,發(fā)現(xiàn)文章把書本中的重點(diǎn)部分都記載出來。所以在這里轉(zhuǎn)載過來,對(duì)應(yīng)著書本的詳解和這篇文章能更清晰的理解Block的實(shí)現(xiàn)原理;

一、何為Block?

  • Block:帶有自動(dòng)變量值的匿名函數(shù)。(匿名函數(shù):沒有函數(shù)名的函數(shù),一對(duì){}包裹的內(nèi)容是匿名函數(shù)的作用域。
    自動(dòng)變量:棧上聲明的一個(gè)變量不是靜態(tài)變量和全局變量,是不可以在這個(gè)棧內(nèi)聲明的匿名函數(shù)中使用的,但在Block中卻可以。)

  • 雖然使用Block不用聲明類,但是Block提供了類似Objective-C的類一樣可以通過成員變量來保存作用域外變量值的方法,那些在Block的一對(duì){}里使用到但卻是在{}作用域以外聲明的變量,就是Block截獲的自動(dòng)變量。

二、Block語(yǔ)法

  1. Block表達(dá)式語(yǔ)法:
  • ^ 返回值類型 (參數(shù)列表) {表達(dá)式}
^ int (int count) {
      return count + 1;
  };

其中返回值類型 參數(shù)都可省略

   ^ {
       NSLog(@"No Parameter");
   };
  1. Block類型變量
  • 聲明Block類型變量語(yǔ)法:

返回值類型 (^變量名)(參數(shù)列表) = Block表達(dá)式

例如,如下聲明了一個(gè)變量名為blk的Block:

    int (^blk)(int) = ^(int count) {
        return count + 1;
    };
  • 當(dāng)Block類型變量作為函數(shù)的參數(shù)時(shí),寫作:
- (void)func:(int (^)(int))blk {
    NSLog(@"Param:%@", blk);
}

借助typedef可簡(jiǎn)寫:

typedef int (^blk_k)(int);

- (void)func:(blk_k)blk {
    NSLog(@"Param:%@", blk);
}

三、截獲自動(dòng)變量值

Block表達(dá)式可截獲所使用的自動(dòng)變量的值。
截獲:保存自動(dòng)變量的瞬間值。
因?yàn)槭恰八查g值”,所以聲明Block之后,即便在Block外修改自動(dòng)變量的值,也不會(huì)對(duì)Block內(nèi)截獲的自動(dòng)變量值產(chǎn)生影響。

  int i = 10;
    void (^blk)(void) = ^{
        NSLog(@"In block, i = %d", i);
    };
    i = 20;//Block外修改變量i,也不影響B(tài)lock內(nèi)的自動(dòng)變量
    blk();//i修改為20后才執(zhí)行,打印: In block, i = 10
    NSLog(@"i = %d", i);//打?。篿 = 20

四、__block說明符號(hào)

自動(dòng)變量值為一個(gè)變量情況
  • 自動(dòng)變量截獲的值為Block聲明時(shí)刻的瞬間值,保存后就不能改寫該值,如需對(duì)自動(dòng)變量進(jìn)行重新賦值,需要在變量聲明前附加__block說明符,這時(shí)該變量稱為__block變量。例如:
  __block int i = 10;//i為__block變量,可在block中重新賦值
    void (^blk)(void) = ^{
        NSLog(@"In block, i = %d", i);
    };
    i = 20;
    blk();//打印: In block, i = 20
    NSLog(@"i = %d", i);//打?。篿 = 20
自動(dòng)變量值為一個(gè)對(duì)象情況
  • 當(dāng)自動(dòng)變量為一個(gè)類的對(duì)象,且沒有使用__block修飾時(shí),雖然不可以在Block內(nèi)對(duì)該變量進(jìn)行重新賦值,但可以修改該對(duì)象的屬性。
    如果該對(duì)象是個(gè)Mutable的對(duì)象,例如NSMutableArray,則還可以在Block內(nèi)對(duì)NSMutableArray進(jìn)行元素的增刪:
    NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:@"1", @"2",nil ];
    NSLog(@"Array Count:%ld", array.count);//打印Array Count:2
    void (^blk)(void) = ^{
        [array removeObjectAtIndex:0];//Ok
        //array = [NSNSMutableArray new];//沒有__block修飾,編譯失??!
    };
    blk();
    NSLog(@"Array Count:%ld", array.count);//打印Array Count:1

Block實(shí)現(xiàn)原理

  • 使用Clang
    Block實(shí)際上是作為極普通的C語(yǔ)言源碼來處理的:含有Block語(yǔ)法的源碼首先被轉(zhuǎn)換成C語(yǔ)言編譯器能處理的源碼,再作為普通的C源代碼進(jìn)行編譯。
    使用LLVM編譯器的clang命令可將含有Block的Objective-C代碼轉(zhuǎn)換成C++的源代碼,以探查其具體實(shí)現(xiàn)方式:
clang -rewrite-objc 源碼文件名

注:如果使用該命令報(bào)錯(cuò):’UIKit/UIKit.h’ file not found,可參考《Objective-C編譯成C++代碼報(bào)錯(cuò)》解決。

  • Block結(jié)構(gòu)
    使用Block的時(shí)候,編譯器對(duì)Block語(yǔ)法進(jìn)行了怎樣的轉(zhuǎn)換?
    int main() {
        int count = 10;
        void (^ blk)() = ^(){
            NSLog(@"In Block:%d", count);
        };
        blk();
    }

如上所示的最簡(jiǎn)單的Block使用代碼,經(jīng)clang轉(zhuǎn)換后,可得到以下幾個(gè)部分(有代碼刪減和注釋添加):

static void __main_block_func_0(
    struct __main_block_impl_0 *__cself) {
    int count = __cself->count; // bound by copy
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_d2f8d2_mi_0, 
    count);

這是一個(gè)函數(shù)的實(shí)現(xiàn),對(duì)應(yīng)Block中{}內(nèi)的內(nèi)容,這些內(nèi)容被當(dāng)做了C語(yǔ)言函數(shù)來處理,函數(shù)參數(shù)中的__cself相當(dāng)于Objective-C中的self。

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc; //描述Block大小、版本等信息
      int count;
      //構(gòu)造函數(shù)函數(shù)
      __main_block_impl_0(void *fp,
              struct __main_block_desc_0 *desc,
              int _count,
              int flags=0) : count(_count) {
        impl.isa = &_NSConcreteStackBlock; //在函數(shù)棧上聲明,則為_NSConcreteStackBlock
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };  

__main_block_impl_0即為main()函數(shù)棧上的Block結(jié)構(gòu)體,其中的__block_impl結(jié)構(gòu)體聲明如下:

    struct __block_impl {
      void *isa;//指明對(duì)象的Class
      int Flags;
      int Reserved;
      void *FuncPtr;
    };

去除掉復(fù)雜的類型轉(zhuǎn)化,可簡(jiǎn)寫為:

    int main() {
        int count = 10;
        sturct __main_block_impl_0 *blk = &__main_block_impl_0(__main_block_func_0,         //函數(shù)指針
                                                               &__main_block_desc_0_DATA)); //Block大小、版本等信息
    
        (*blk->FuncPtr)(blk);   //調(diào)用FuncPtr指向的函數(shù),并將blk自己作為參數(shù)傳入
    }

由此,可以看出,Block也是Objective-C中的對(duì)象。
Block有三種類(即__block_impl的isa指針指向的值,isa說明參考《Objective-C isa 指針 與 runtime 機(jī)制》),根據(jù)Block對(duì)象創(chuàng)建時(shí)所處數(shù)據(jù)區(qū)不同而進(jìn)行區(qū)別:

  • _NSConcreteStackBlock:在棧上創(chuàng)建的Block對(duì)象
  • _NSConcreteMallocBlock:在堆上創(chuàng)建的Block對(duì)象
  • _NSConcreteGlobalBlock:全局?jǐn)?shù)據(jù)區(qū)的Block對(duì)象

一、如何截獲自動(dòng)變量

  • 上部分介紹了Block的結(jié)構(gòu),和作為匿名函數(shù)的調(diào)用機(jī)制,那自動(dòng)變量截獲是發(fā)生在什么時(shí)候呢?
    觀察上節(jié)代碼中__main_block_impl_0結(jié)構(gòu)體(main棧上Block的結(jié)構(gòu)體)的構(gòu)造函數(shù)可以看到,棧上的變量count以參數(shù)的形式傳入到了這個(gè)構(gòu)造函數(shù)中,此處即為變量的自動(dòng)截獲。
    因此可以這樣理解:__block_impl結(jié)構(gòu)體已經(jīng)可以代表Block類了,但在棧上又聲明了__main_block_impl_0結(jié)構(gòu)體,對(duì)__block_impl進(jìn)行封裝后才來表示棧上的Block類,就是為了獲取Block中使用到的棧上聲明的變量(棧上沒在Block中使用的變量不會(huì)被捕獲),變量被保存在Block的結(jié)構(gòu)體實(shí)例中。
    所以在blk()執(zhí)行之前,棧上簡(jiǎn)單數(shù)據(jù)類型的count無(wú)論發(fā)生什么變化,都不會(huì)影響到Block以參數(shù)形式傳入而捕獲的值。但這個(gè)變量是指向?qū)ο蟮闹羔槙r(shí),是可以修改這個(gè)對(duì)象的屬性的,只是不能為變量重新賦值。

二、Block的存儲(chǔ)域

  • 上文已提到,根據(jù)Block創(chuàng)建的位置不同,Block有三種類型,創(chuàng)建的Block對(duì)象分別會(huì)存儲(chǔ)到棧、堆、全局?jǐn)?shù)據(jù)區(qū)域。
    void (^blk)(void) = ^{
        NSLog(@"Global Block");
    };
    int main() {
        blk();
        NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__
    }
  • 像上面代碼塊中的全局blk自然是存儲(chǔ)在全局?jǐn)?shù)據(jù)區(qū),但注意在函數(shù)棧上創(chuàng)建的blk,如果沒有截獲自動(dòng)變量,Block的結(jié)構(gòu)實(shí)例還是會(huì)被設(shè)置在程序的全局?jǐn)?shù)據(jù)區(qū),而非棧上:
    int main() {
        void (^blk)(void) = ^{//沒有截獲自動(dòng)變量的Block
            NSLog(@"Stack Block");
        };
        blk();
        NSLog(@"%@",[blk class]);//打印:__NSGlobalBlock__
        
        int i = 1;
        void (^captureBlk)(void) = ^{//截獲自動(dòng)變量i的Block
            NSLog(@"Capture:%d", i);
        };
        captureBlk();
        NSLog(@"%@",[captureBlk class]);//打?。篲_NSMallocBlock__
    }

可以看到截獲了自動(dòng)變量的Block打印的類是NSGlobalBlock,表示存儲(chǔ)在全局?jǐn)?shù)據(jù)區(qū)。
但為什么捕獲自動(dòng)變量的Block打印的類卻是設(shè)置在堆上的NSMallocBlock,而非棧上的NSStackBlock?這個(gè)問題稍后解釋。

三、Block復(fù)制

  • 配置在棧上的Block,如果其所屬的棧作用域結(jié)束,該Block就會(huì)被廢棄,對(duì)于超出Block作用域仍需使用Block的情況,Block提供了將Block從棧上復(fù)制到堆上的方法來解決這種問題,即便Block棧作用域已結(jié)束,但被拷貝到堆上的Block還可以繼續(xù)存在。
    復(fù)制到堆上的Block,將_NSConcreteMallocBlock類對(duì)象寫入Block結(jié)構(gòu)體實(shí)例的成員變量isa:
impl.isa = &_NSConcreteMallocBlock;

在ARC有效時(shí),大多數(shù)情況下編譯器會(huì)進(jìn)行判斷,自動(dòng)生成將Block從棧上復(fù)制到堆上的代碼,以下幾種情況棧上的Block會(huì)自動(dòng)復(fù)制到堆上:

調(diào)用Block的copy方法
將Block作為函數(shù)返回值時(shí)
將Block賦值給__strong修改的變量時(shí)
向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數(shù)時(shí)
  • 其它時(shí)候向方法的參數(shù)中傳遞Block時(shí),需要手動(dòng)調(diào)用copy方法復(fù)制Block。
    上一節(jié)的棧上截獲了自動(dòng)變量i的Block之所以在棧上創(chuàng)建,卻是NSMallocBlock類,就是因?yàn)檫@個(gè)Block對(duì)象賦值給了_strong修飾的變量captureBlk(_strong是ARC下對(duì)象的默認(rèn)修飾符)。
    因?yàn)樯厦嫠臈l規(guī)則,在ARC下其實(shí)很少見到_NSConcreteStackBlock類的Block,大多數(shù)情況編譯器都保證了Block是在堆上創(chuàng)建的,如下代碼所示,僅最后一行代碼直接使用一個(gè)不賦值給變量的Block,它的類才是NSStackBlock:
    int count = 0;
    blk_t blk = ^(){
        NSLog(@"In Stack:%d", count);
    };
    
    NSLog(@"blk's Class:%@", [blk class]);//打?。篵lk's Class:__NSMallocBlock__
    NSLog(@"Global Block:%@", [^{NSLog(@"Global Block");} class]);//打印:Global Block:__NSGlobalBlock__
    NSLog(@"Copy Block:%@", [[^{NSLog(@"Copy Block:%d",count);} copy] class]);//打印:Copy Block:__NSMallocBlock__
    NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",count);} class]);//打?。篠tack Block:__NSStackBlock__

關(guān)于ARC下和MRC下Block自動(dòng)copy的區(qū)別,查看《Block 小測(cè)驗(yàn)》里幾道題目就能區(qū)分了。
另外,原書存在ARC和MRC混合講解、區(qū)分不明的情況,比如書中幾個(gè)使用到棧上對(duì)象導(dǎo)致Crash的例子是MRC條件下才會(huì)發(fā)生的,但書中沒做特殊說明。

四、使用__block發(fā)生了什么

  • Block捕獲的自動(dòng)變量添加__block說明符,就可在Block內(nèi)讀和寫該變量,也可以在原來的棧上讀寫該變量。
    自動(dòng)變量的截獲保證了棧上的自動(dòng)變量被銷毀后,Block內(nèi)仍可使用該變量。
    __block保證了棧上和Block內(nèi)(通常在堆上)可以訪問和修改“同一個(gè)變量”,__block是如何實(shí)現(xiàn)這一功能的?

  • __block發(fā)揮作用的原理:將棧上用__block修飾的自動(dòng)變量封裝成一個(gè)結(jié)構(gòu)體,讓其在堆上創(chuàng)建,以方便從棧上或堆上訪問和修改同一份數(shù)據(jù)。

  • 驗(yàn)證過程:
    現(xiàn)在對(duì)剛才的代碼段,加上__block說明符,并在block內(nèi)外讀寫變量count。

int main() {
    __block int count = 10;
    void (^ blk)() = ^(){
        count = 20;
        NSLog(@"In Block:%d", count);//打?。篒n Block:20
    };
    count ++;
    NSLog(@"Out Block:%d", count);//打?。篛ut Block:11
    blk();

將上面的代碼段clang,發(fā)現(xiàn)Block的結(jié)構(gòu)體__main_block_impl_0結(jié)構(gòu)如下所示:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_count_0 *count; // by ref
  __main_block_impl_0(void *fp, struct __main_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;
  }

最大的變化就是count變量不再是int類型了,count變成了一個(gè)指向__Block_byref_count_0結(jié)構(gòu)體的指針,__Block_byref_count_0結(jié)構(gòu)如下:

struct __Block_byref_count_0 {
  void *__isa;
__Block_byref_count_0 *__forwarding;
 int __flags;
 int __size;
 int count;
};

它保存了int count變量,還有一個(gè)指向__Block_byref_count_0實(shí)例的指針__forwarding,通過下面兩段代碼__forwarding指針的用法可以知道,該指針其實(shí)指向的是對(duì)象自身:
//Block的執(zhí)行函數(shù)

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_count_0 *count = __cself->count; // bound by ref
 
        (count->__forwarding->count) = 20;//對(duì)應(yīng)count = 20;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_0, 
        (count->__forwarding->count));
    }

//main函數(shù)
int main() {
    __attribute__((__blocks__(byref))) __Block_byref_count_0 count = {(void*)0,
    (__Block_byref_count_0 *)&count, 
    0, 
    sizeof(__Block_byref_count_0), 
    10};
    
    void (* blk)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, 
    &__main_block_desc_0_DATA, 
    (__Block_byref_count_0 *)&count, 
    570425344));
    
    (count.__forwarding->count) ++;//對(duì)應(yīng)count ++;
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_64_vf2p_jz52yz7x4xtcx55yv0r0000gn_T_main_fafeeb_mi_1, 
    (count.__forwarding->count));
    
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
}

為什么要通過__forwarding指針完成對(duì)count變量的讀寫修改?
為了保證無(wú)論是在棧上還是在堆上,都能通過都__forwarding指針找到在堆上創(chuàng)建的count這個(gè)__main_block_func_0結(jié)構(gòu)體,以完成對(duì)count->count(第一個(gè)count是__main_block_func_0對(duì)象,第二個(gè)count是int類型變量)的訪問和修改。

五、Block的循環(huán)引用

  • Block的循環(huán)引用原理和解決方法大家都比較熟悉,此處將結(jié)合上文的介紹,介紹一種不常用的解決Block循環(huán)引用的方法和一種借助Block參數(shù)解決該問題的方法。
    Block循環(huán)引用原因:一個(gè)對(duì)象A有Block類型的屬性,從而持有這個(gè)Block,如果Block的代碼塊中使用到這個(gè)對(duì)象A,或者僅僅是用用到A對(duì)象的屬性,會(huì)使Block也持有A對(duì)象,導(dǎo)致兩者互相持有,不能在作用域結(jié)束后正常釋放。
    解決原理:對(duì)象A照常持有Block,但Block不能強(qiáng)引用持有對(duì)象A以打破循環(huán)。
    解決方法:
    方法一: 對(duì)Block內(nèi)要使用的對(duì)象A使用__weak進(jìn)行修飾,Block對(duì)對(duì)象A弱引用打破循環(huán)。

有三種常用形式:

  1. 使用__weak ClassName
    __block XXViewController* weakSelf = self;
    self.blk = ^{
        NSLog(@"In Block : %@",weakSelf);
    };

2.使用__weak typeof(self)

    __weak typeof(self) weakSelf = self;
    self.blk = ^{
        NSLog(@"In Block : %@",weakSelf);
    };

3.Reactive Cocoa中的@weakify和@strongify

    @weakify(self);
    self.blk = ^{
        @strongify(self);
        NSLog(@"In Block : %@",self);
    };

其原理參考《@weakify, @strongify》,自己簡(jiǎn)便實(shí)現(xiàn)參考《@weak - @strong 宏的實(shí)現(xiàn)》

方法二:對(duì)Block內(nèi)要使用的對(duì)象A使用__block進(jìn)行修飾,并在代碼塊內(nèi),使用完__block變量后將其設(shè)為nil,并且該block必須至少執(zhí)行一次。

   __block XXController *blkSelf = self;
        self.blk = ^{
            NSLog(@"In Block : %@",blkSelf);
            blkSelf = nil;//不能省略
        };
   self.blk();//該block必須執(zhí)行一次,否則還是內(nèi)存泄露

在block代碼塊內(nèi),使用完使用完__block變量后將其設(shè)為nil,并且該block必須至少執(zhí)行一次后,不存在內(nèi)存泄露,因?yàn)榇藭r(shí):

XXController對(duì)象持有Block對(duì)象blk
blk對(duì)象持有__block變量blkSelf(類型為編譯器創(chuàng)建的結(jié)構(gòu)體)
__block變量blkSelf在執(zhí)行blk()之后被設(shè)置為nil(__block變量結(jié)構(gòu)體的__forwarding指針指向了nil),不再持有XXController對(duì)象,打破循環(huán)

第二種使用__block打破循環(huán)的方法,優(yōu)點(diǎn)是:

可通過__block變量動(dòng)態(tài)控制持有XXController對(duì)象的時(shí)間,運(yùn)行時(shí)決定是否將nil或其他變量賦值給__block變量
不能使用__weak的系統(tǒng)中,使用__unsafe_unretained來替代__weak打破循環(huán)可能有野指針問題,使用__block則可避免該問題

其缺點(diǎn)也明顯:

必須手動(dòng)保證__block變量最后設(shè)置為nil
block必須執(zhí)行一次,否則__block不為nil循環(huán)應(yīng)用仍存在

因此,還是避免使用第二種不常用方式,直接使用__weak打破Block循環(huán)引用。
方法三:將在Block內(nèi)要使用到的對(duì)象(一般為self對(duì)象),以Block參數(shù)的形式傳入,Block就不會(huì)捕獲該對(duì)象,而將其作為參數(shù)使用,其生命周期系統(tǒng)的棧自動(dòng)管理,不造成內(nèi)存泄露。
即原來使用__weak的寫法:

  __weak typeof(self) weakSelf = self;
  self.blk = ^{
      __strong typeof(self) strongSelf = weakSelf;
      NSLog(@"Use Property:%@", strongSelf.name);
      //……
  };
  self.blk();

改為Block傳參寫法后:

    self.blk = ^(UIViewController *vc) {
        NSLog(@"Use Property:%@", vc.name);
    };
    self.blk(self);

優(yōu)點(diǎn):

  • 簡(jiǎn)化了兩行代碼,更優(yōu)雅
  • 更明確的API設(shè)計(jì):告訴API使用者,該方法的Block直接使用傳進(jìn)來的參數(shù)對(duì)象,不會(huì)造成循環(huán)引用,不用調(diào)用者再使用weak避免循環(huán)

該種用法的詳細(xì)思路,和clang后的數(shù)據(jù)結(jié)構(gòu),可參考《Heap-Stack Dance》。

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

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

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