Block詳解

一、Block本質(zhì)

Block是“帶有自動(dòng)變量值的匿名函數(shù)”。

所謂的匿名函數(shù)就是不帶有名稱的函數(shù)

typedef int (^blk_t)(int)
blk_t = ^(int count){
  return count+1;
}

但它究竟是什么呢?

轉(zhuǎn)碼

通過(guò)-rewrite-objc選項(xiàng)將含有Block語(yǔ)法的源代碼變換為C++代碼

變換前:

#include<stdio.h>
int main() {
    void (^blk)(void) = ^{printf("Block");};
    blk();
    return 0;
}

終端:clang -rewrite-objc 源代碼文件名

變換后:(變換后有568行,精簡(jiǎn)后如下)

struct __block_impl {
  void *isa; //isa指針
  int Flags; //標(biāo)志位
  int Reserved; 
  void *FuncPtr; //函數(shù)指針
};

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size; //block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc; //block描述信息
  //構(gòu)造函數(shù)(類似于OC的init方法),返回結(jié)構(gòu)體對(duì)象
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock; //isa指針
    impl.Flags = flags; //標(biāo)志位
    impl.FuncPtr = fp;//函數(shù)指針
    Desc = desc; //block描述信息
  }
};

/*
  ^{printf("Block");};變換后的樣子
  Block匿名函數(shù)實(shí)際上被作為簡(jiǎn)單的C函數(shù)來(lái)處理
  函數(shù)名的命名規(guī)則:根據(jù)Block語(yǔ)法所在的函數(shù)名(此處為mian)和該Block語(yǔ)法在該函數(shù)出現(xiàn)的順序值(此處為0)來(lái)命名的
 __cself相當(dāng)于OC中的self 
*/
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
        printf("Block");
}

int main() {
    /*
    void (^blk)(void) = ^{printf("Block");};轉(zhuǎn)換后的代碼
    
    構(gòu)造函數(shù)構(gòu)造后,__main_block_impl_0結(jié)構(gòu)體結(jié)果如下
    isa = &_NSConcreteStackBlock;
    Flags = 0;
    FuncPtr = __main_block_func_0; //函數(shù)指針,指向__main_block_func_0函數(shù)
    Desc = &__main_block_desc_0_DATA;
    */
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
  
    /*
      blk();轉(zhuǎn)換后的代碼如下
    */
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

分析

1、分析void (^blk)(void) = ^{printf("Block");}

上面C++代碼

void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

去掉轉(zhuǎn)換后的部分如下

//創(chuàng)建一個(gè)結(jié)構(gòu)體實(shí)例
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA));
//將結(jié)構(gòu)體實(shí)例的指針賦給blk
struct __main_block_impl_0 * blk = &tmp;

通過(guò)簡(jiǎn)化后的代碼可知,源代碼將__main_block_impl_0結(jié)構(gòu)體類型的自動(dòng)變量,即棧上生成的__main_block_impl_0結(jié)構(gòu)體實(shí)例的指針,賦值給__main_block_impl_0結(jié)構(gòu)體指針類型的變量blk。

而最初的Block源代碼是void (^blk)(void) = ^{printf("Block");};

因此,將Block語(yǔ)法生成的Block賦給Block類型變量blk。它等同于將__main_block_impl_0結(jié)構(gòu)體實(shí)例的指針賦給變量blk。

  • 堆:動(dòng)態(tài)分配內(nèi)存,需要程序員自己申請(qǐng),程序員自己管理

  • 棧:自動(dòng)分配內(nèi)存,自動(dòng)銷毀,先入后出,棧上的內(nèi)容存在自動(dòng)銷毀的情況

2、分析blk()

blk();轉(zhuǎn)換后的代碼如下:

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

去掉轉(zhuǎn)換后的部分如下:

(*blk->impl.FuncPtr)(blk);

可見(jiàn),這就是簡(jiǎn)單地使用函數(shù)指針調(diào)用函數(shù)。__main_block_func_0的函數(shù)指針被賦值到成員變量FuncPtr中。另外也說(shuō)明了,__main_block_func_0函數(shù)的參數(shù)__cself指向Block值。在調(diào)用該函數(shù)的源代碼中可以看出Block正是作為參數(shù)進(jìn)行傳遞。

__main_block_impl_0結(jié)構(gòu)體相當(dāng)于基于objc_object結(jié)構(gòu)體的Objective-C類對(duì)象的結(jié)構(gòu)體。

_NSConcreteStackBlock相當(dāng)于class_t結(jié)構(gòu)體實(shí)例。在將Block作為Objective-C對(duì)象處理時(shí),關(guān)于該類的信息放置于_NSConcreteStackBlock中。

因此,Block本質(zhì)上也是一個(gè)OC對(duì)象(最終繼承NSObject),它內(nèi)部也有個(gè)isa指針。

二、Block捕獲變量值

Block是帶有自變量的匿名函數(shù),其中的"帶有自變量值"是什么意思呢?"帶有自變量值"在block中表現(xiàn)為"捕獲自變量值"。

為了保證Block內(nèi)部能夠正常訪問(wèn)外部的變量,Block有個(gè)變量捕獲機(jī)制:

局部變量(auto):捕獲到Block內(nèi),值傳遞;

局部變量(static):捕獲到Block內(nèi),指針傳遞;

全局變量:不捕獲到Block內(nèi),直接訪問(wèn);

Q:下列代碼輸出值分別為多少?

  int val = 10;
  const char * fmt = "val = %d\n";
  void (^blk)(void) = ^{
    printf(fmt,val);
  };
  
  val = 2;
  fmt = "Change the value of val,val = %d\n";
  
  blk();

輸出結(jié)果為:val = 10

原因:在執(zhí)行Block語(yǔ)法時(shí),會(huì)捕獲自動(dòng)變量值,即Block語(yǔ)法表達(dá)式所使用到的自動(dòng)變量值被保存到Block的結(jié)構(gòu)體實(shí)例中。

源碼證明:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  int val;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy
  printf(fmt,val);
}

int main() {
    int val = 10;
    const char * fmt = "val = %d\n";
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));

    val = 2;
    fmt = "Change the value of val,val = %d\n";

    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

Q:下列代碼輸出值分別為多少?

auto int age = 10;
static int num = 25;
void (^Block)(void) = ^{
    printf("age:%d,num:%d",age,num);
};
age = 20;
num = 11;
Block();

輸出結(jié)果為:age:10,num:11
原因:auto變量Block訪問(wèn)方式是值傳遞,static變量Block訪問(wèn)方式是指針傳遞

源碼證明:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  int *num;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_num, int flags=0) : age(_age), num(_num) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int age = __cself->age; // bound by copy
  int *num = __cself->num; // bound by copy
  printf("age:%d,num:%d",age,(*num));
}

int main() {
    auto int age = 10;
    static int num = 25;
    void (*Block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &num));
    age = 20;
    num = 11;
    ((void (*)(__block_impl *))((__block_impl *)Block)->FuncPtr)((__block_impl *)Block);
    return 0;
}

上述代碼可知static修飾的變量,是根據(jù)指針訪問(wèn)的

Q:為什么block對(duì)auto和static變量捕獲有差異?

auto自動(dòng)變量可能會(huì)銷毀的,內(nèi)存可能會(huì)消失,不采用指針訪問(wèn);static變量一直保存在內(nèi)存中,指針訪問(wèn)即可

三、__block說(shuō)明符

__block說(shuō)明符類似于static、auto和register說(shuō)明符,它們用于指定將變量值設(shè)置到哪個(gè)存儲(chǔ)域中(參見(jiàn)下文)。如auto表示作為自動(dòng)變量存儲(chǔ)在棧中,static表示作為靜態(tài)變量存儲(chǔ)在數(shù)據(jù)區(qū)中。

自變量被Block截獲后,Block保存的是當(dāng)前的瞬間值,保存后就不能修改該值。若想在Block語(yǔ)法中修改捕獲到的自變量的值,則需要在該值變量前加上__block說(shuō)明符,如果不加該說(shuō)明符,則運(yùn)行會(huì)報(bào)錯(cuò)

__block int val = 0;
void (^blk)(void) = ^{
    val = 1; //修改捕獲到的自變量的值
};

blk();
printf("val = %d\n",val); 

系統(tǒng)對(duì)__block int val = 0;做了什么?

編譯器會(huì)將__block變量包裝成一個(gè)對(duì)象,具體的C++代碼如下:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding; //val的地址
 int __flags;
 int __size;
 int val; //val的值
};

從C++代碼可知,結(jié)構(gòu)體持有相當(dāng)于原自動(dòng)變量的成員變量。通過(guò)成員變量__forwarding訪問(wèn)成員變量val。(成員變量val是該實(shí)例自身持有的變量,它相當(dāng)于原自動(dòng)變量本身),如下圖:


訪問(wèn)__block變量.png

因此,加了__block修飾的變量,Block截獲后是通過(guò)指針去操作該變量,因此可以修改變量的值。

棧上__block__forwarding指向本身

棧上__block復(fù)制到堆上后,棧上block的__forwarding指向堆上的block,堆上block的__forwarding指向本身

復(fù)制__block變量后的變化.png

捕獲OC對(duì)象(不用__block修飾),調(diào)用變更對(duì)象的方法是可以的,而向捕獲的變量賦值則會(huì)產(chǎn)生編譯錯(cuò)誤

id arr = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
  id obj = [[NSObject alloc] init];
  [arr addObject:obj]; //這樣是可以的
  
  arr = [[NSMutableArray alloc] init]; //這樣是不行的(編譯報(bào)錯(cuò))
}

現(xiàn)在的Block中,捕獲自變量的方法并沒(méi)有實(shí)現(xiàn)對(duì)C語(yǔ)言數(shù)組的截獲,因此在訪問(wèn)C語(yǔ)言數(shù)組時(shí)會(huì)產(chǎn)生編譯錯(cuò)誤,可以通過(guò)使用指針解決該問(wèn)題

const char text[] = "hello";
void (^blk)(void) = ^{
  printf("%c\n",text[2]); //這樣使用時(shí)不行的
};

const char *text = "hello";
void (^blk)(void) = ^{
  printf("%c\n",text[2]); //改成指針可以
};

__block總結(jié)

  • __block可以用于解決block內(nèi)部無(wú)法修改auto變量值的問(wèn)題
  • __block不能修飾全局變量、靜態(tài)變量(static)
  • 當(dāng)__block變量在棧上時(shí),不會(huì)對(duì)指向的對(duì)象產(chǎn)生強(qiáng)引用
  • 編譯器會(huì)將__block變量包裝成一個(gè)對(duì)象
  • __block修改變量:age->forwarding->age
  • __Block_byref_val_0結(jié)構(gòu)體內(nèi)部地址和外部變量val是同一地址

四、Block類型

Block的類型取決于isa指針,可以通過(guò)調(diào)用class方法查看具體類型,最終都繼承自NSBlock。

  • __NSGlobalBlock __ ( _NSConcreteGlobalBlock )
  • __NSStackBlock __ ( _NSConcreteStackBlock )
  • __NSMallocBlock __ ( _NSConcreteMallocBlock )

代碼示例

void (^block1)(void) = ^{
    NSLog(@"block1");
};
NSLog(@"%@",[block1 class]);
NSLog(@"%@",[[block1 class] superclass]);
NSLog(@"%@",[[[block1 class] superclass] superclass]);
NSLog(@"%@",[[[[block1 class] superclass] superclass] superclass]);
NSLog(@"%@",[[[[[block1 class] superclass] superclass] superclass] superclass]);

輸出結(jié)果:
NSGlobalBlock
__NSGlobalBlock
NSBlock
NSObject
null

上述代碼輸出了block1的類型,也證實(shí)了block是對(duì)象,最終繼承NSObject

代碼展示block的三種類型:

    /*
    全局block
    沒(méi)有訪問(wèn)auto變量的block是__NSGlobalBlock__,放在數(shù)據(jù)段
    因?yàn)樵谑褂萌肿兞康牡胤讲荒苁褂米詣?dòng)變量,所有不存在對(duì)自動(dòng)變量進(jìn)行截獲
    由于此Block結(jié)構(gòu)體實(shí)例的內(nèi)容不依賴于執(zhí)行時(shí)的狀態(tài),所以整個(gè)程序中只需一個(gè)實(shí)例
    因此,將Block結(jié)構(gòu)體實(shí)例設(shè)置在與全局變量相同的數(shù)據(jù)區(qū)域中即可
    */
    void (^blk1)(void) = ^{ NSLog(@"blk1"); };
    NSLog(@"%@",[blk1 class]);
    
    /*
     堆block
     將Block賦值給__strong指針時(shí),ARC環(huán)境下,編譯器會(huì)根據(jù)情況自動(dòng)將棧上的Block復(fù)制到堆上
     如果void (^blk2)(void) = ^{  寫成  void __weak (^blk2)(void) = ^{
     則是棧block(編譯器沒(méi)有將其復(fù)制到堆上)
     */
    int age = 1;
    void (^blk2)(void) = ^{
        NSLog(@"blk2:%d",age);
    };
    NSLog(@"%@",[blk2 class]);
    
    /*
     棧block
     訪問(wèn)了變量,并且沒(méi)有做copy操作
     */
    NSLog(@"%@",[^{
        NSLog(@"blk3:%d",age);
    } class]);

輸出結(jié)果:
NSGlobalBlock

NSMallocBlock

NSStackBlock

  • __NSGlobalBlock __ 在數(shù)據(jù)區(qū)

  • __NSMallocBlock __ 在堆區(qū)

  • __NSStackBlock __ 在棧區(qū)

  • 堆:動(dòng)態(tài)分配內(nèi)存,需要程序員自己申請(qǐng),程序員自己管理

  • 棧:自動(dòng)分配內(nèi)存,自動(dòng)銷毀,先入后出,棧上的內(nèi)容存在自動(dòng)銷毀的情況

    Block存儲(chǔ)域.png

    如何判斷Block是哪種類型

  • 沒(méi)有訪問(wèn)auto變量的block是__NSGlobalBlock __ ,放在數(shù)據(jù)段

  • 訪問(wèn)了auto變量的block是__NSStackBlock __

  • [__NSStackBlock __ copy]操作就變成了__NSMallocBlock __

在ARC環(huán)境下,編譯器會(huì)根據(jù)情況自動(dòng)將棧上的Block復(fù)制到堆上的情況有如下幾種

  • Block作為函數(shù)返回值返回時(shí),編譯器會(huì)自動(dòng)生成復(fù)制到堆上的代碼;
  • 將Block賦值給__strong指針時(shí);
  • Cocoa框架的方法,并且方法名中包含有usingBlock等時(shí);
  • Block作為GCD API的方法參數(shù)時(shí);

如在使用NSArray類的enumerateObjectsUsingBlock實(shí)例方法以及dispatch_async函數(shù)時(shí),不用手動(dòng)復(fù)制。相反

在NSArray類的initWithObjects實(shí)例方法上傳遞Block時(shí)需要手動(dòng)復(fù)制。

備注:將Block從棧上復(fù)制到堆上是相當(dāng)消耗CPU的。

對(duì)每種類型Block調(diào)用copy操作后是什么結(jié)果?

Block調(diào)用copy的情況.png

不管Block配置在何處,用copy方法復(fù)制都不會(huì)引起任何問(wèn)題,在不確定時(shí)調(diào)用copy方法即可。

__block變量存儲(chǔ)域

__block變量的存儲(chǔ)域.png

若一個(gè)Block中使用__block變量,則當(dāng)該Block從棧復(fù)制到堆時(shí),由于其使用的所有__block變量也必定配置在棧上。所以這些__block變量也會(huì)一并從棧復(fù)制到堆,并且被該Block所持有。
復(fù)制__block變量.png

當(dāng)多個(gè)Block使用同一個(gè)__block變量時(shí),因?yàn)锽lock最先是配置在棧上的,所以__block變量也都配置在棧上。當(dāng)其中一個(gè)Block被復(fù)制到堆上時(shí),__block變量也會(huì)一并從棧復(fù)制到堆,并被Block所持有。當(dāng)其他的Block從棧復(fù)制到堆時(shí),被復(fù)制的Block持有__block變量,并增加__block變量的引用計(jì)數(shù)。如下圖:
多個(gè)block持有同一個(gè)__block變量..png

如果配置在堆上的Block被廢棄,那么它所使用的__block變量也就被釋放。

MRC下Block屬性的建議寫法
@property (copy, nonatomic) void (^block)(void);

ARC下Block屬性的建議寫法
@property (copy, nonatomic) void (^block)(void);

Block的屬性修飾詞為什么是copy

Block需要通過(guò)copy才會(huì)被復(fù)制到堆上,只有在堆上,程序員才能對(duì)它做內(nèi)存管理、控制Block生命周期等操作。

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

__strong強(qiáng)引用

__strong修飾符是id類型和對(duì)象類型,默認(rèn)的所有權(quán)修飾符,可以不寫

__weak弱引用

__weak弱引用,不持有對(duì)象,在超出其變量作用域時(shí)(如函數(shù)花括號(hào)之外),對(duì)象立即被釋放

__weak可以避免循環(huán)強(qiáng)引用

__weak在持有某對(duì)象的弱引用時(shí),若該對(duì)象被廢棄,則此弱引用將自動(dòng)失效,且被置為nil

__weak修飾符只能在iOS5以上使用

__unsafe_unretained

在iOS5以下用__unsafe_unretained修飾符

盡管ARC式的內(nèi)存管理是編譯器的工作,但符有__unsafe_unretained修飾符的變量不屬于編譯器的內(nèi)存管理對(duì)象。

賦值給__unsafe_unretained修飾的變量的對(duì)象,需確保對(duì)象不為空,否則會(huì)產(chǎn)生懸垂指針,導(dǎo)致運(yùn)行奔潰。

如下:

id __unsafe_unretained obj1 = nil
{
    id __strong obj0 = [[NSObject alloc] init];  //obj0持有對(duì)象
  obj1 = obj0; //雖然obj0變量賦給obj1,但obj1變量既不持有對(duì)象的強(qiáng)引用,也不持有對(duì)象的弱引用
  NSLog(@"A:%@",obj1);
}
//obj0超出其作用域,強(qiáng)引用失效,所以自動(dòng)釋放自己持有的對(duì)象,因?yàn)閇[NSObject alloc] init]對(duì)象沒(méi)有持有者,所以廢棄該對(duì)象

NSLog(@"B:%@",obj1);
//obj1變量表示的對(duì)象已被廢棄(懸垂指針),因此訪問(wèn)出錯(cuò)

@autoreleasepool自動(dòng)釋放池

@autoreleasepool {
    id __strong obj = [NSMutableArray array];
} //到這個(gè)花括號(hào)釋放池代碼塊結(jié)束,隨著@autoreleasepool塊的結(jié)束,注冊(cè)到autoreleasepool中的所有對(duì)象被自動(dòng)釋放

解決循環(huán)引用:

__weak:不會(huì)產(chǎn)生強(qiáng)引用,指向的對(duì)象銷毀時(shí),會(huì)自動(dòng)讓指針置為nil

    MyObject * object = [[MyObject alloc] init];
    object.age = 10;
    __weak typeof(object) weakObject = object;
    object.blk = ^{
        NSLog(@"age is %d", weakObject.age);
    };
    object.blk();

__block:必須把引用對(duì)象置為nil,并且要調(diào)用該block

    __block MyObject * object = [[MyObject alloc] init];
    object.age = 10;
    object.blk = ^{
        NSLog(@"age is %d", object.age);
        object = nil;
    };
    object.blk();

參考資料

Objective-C高級(jí)編程

最后編輯于
?著作權(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)容

  • 第一部分:Block本質(zhì) Q:什么是Block,Block的本質(zhì)是什么? block本質(zhì)上也是一個(gè)OC對(duì)象,它內(nèi)部...
    sheldon_龍閱讀 607評(píng)論 0 0
  • __block說(shuō)明符 Block只能保存局部變量瞬間的值,所以當(dāng)我們嘗試修改截獲的自動(dòng)變量值,就會(huì)報(bào)錯(cuò)。例如: 該...
    CharmecarWang閱讀 143評(píng)論 0 1
  • Block概要 Block:帶有自動(dòng)變量的匿名函數(shù)。 匿名函數(shù):沒(méi)有函數(shù)名的函數(shù),一對(duì){}包裹的內(nèi)容是匿名函數(shù)的作...
    zweic閱讀 550評(píng)論 0 2
  • 轉(zhuǎn)自李峰峰博客 一、概述 閉包 = 一個(gè)函數(shù)「或指向函數(shù)的指針」+ 該函數(shù)執(zhí)行的外部的上下文變量「也就是自由變量」...
    Joshua520閱讀 1,105評(píng)論 0 0
  • 開(kāi)始之前,我想先提幾個(gè)問(wèn)題,看看大家是否對(duì)此有疑惑。唐巧已經(jīng)寫過(guò)一篇對(duì)block很有研究的文章,大家可以去看看(本...
    高思陽(yáng)閱讀 1,826評(píng)論 0 1

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