iOS Block原理探究以及循環(huán)引用的問(wèn)題

《Objective-C高級(jí)編程》這本書就講了三個(gè)東西:自動(dòng)引用計(jì)數(shù)、block、GCD,偏向于從原理上對(duì)這些內(nèi)容進(jìn)行講解而且涉及到一些比較底層的實(shí)現(xiàn),再加上因?yàn)橹形姆g以及內(nèi)容條理性等方面的原因,書本有些內(nèi)容比較晦澀難懂,在初初讀的時(shí)候一臉懵逼。本文是對(duì)書中block一章的內(nèi)容做的一些筆記,所以側(cè)重的是講原理,同時(shí)也會(huì)對(duì)書中講得晦澀或不合理的地方相對(duì)進(jìn)行一些補(bǔ)充和擴(kuò)展。

1.Block結(jié)構(gòu)與實(shí)質(zhì)

使用Block的時(shí)候,編譯器對(duì)Block做了怎樣的轉(zhuǎn)換?
分析工具clang
例1

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    void (^blk)(void) = ^{
        NSLog(@"hello");
    };
    blk();
    
    return 0;
}

clang:

//block實(shí)現(xiàn)結(jié)構(gòu)體
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

//block結(jié)構(gòu)體
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//block代碼塊中的實(shí)現(xiàn)
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_f871c6_mi_0);
    }

//block描述結(jié)構(gòu)體
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, const char * argv[]) {
//block實(shí)現(xiàn)
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
//block調(diào)用
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}

從main函數(shù)入手,對(duì)應(yīng)OC的代碼,里面一共做了兩件事:實(shí)現(xiàn)block、調(diào)用block。

1.實(shí)現(xiàn)block

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

它調(diào)用了__main_block_impl_0結(jié)構(gòu)體的構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)。__main_block_impl_0結(jié)構(gòu)體有兩個(gè)成員變量,分別是__block_impl結(jié)構(gòu)體和__main_block_desc_0結(jié)構(gòu)體。

// impl結(jié)構(gòu)體
struct __block_impl {
  void *isa;  // 存儲(chǔ)位置,_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock
  int Flags;  // 按位表示一些 block 的附加信息
  int Reserved;  // 保留變量
  void *FuncPtr;  // 函數(shù)指針,指向 Block 要執(zhí)行的函數(shù),即__main_block_func_0
};

// Desc結(jié)構(gòu)體
static struct __main_block_desc_0 {
  size_t reserved;  // 結(jié)構(gòu)體信息保留字段
  size_t Block_size;  // 結(jié)構(gòu)體大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

再來(lái)看__main_block_impl_0結(jié)構(gòu)體的構(gòu)造函數(shù)

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

第一個(gè)參數(shù)需要傳入一個(gè)函數(shù)指針,第二個(gè)參數(shù)是作為靜態(tài)全局變量初始化的__main_block_desc_0結(jié)構(gòu)體實(shí)例指針,第三個(gè)參數(shù)flags有默認(rèn)值0。重點(diǎn)看第一個(gè)參數(shù),實(shí)際調(diào)用中傳入的是__main_block_func_0函數(shù)指針:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_f871c6_mi_0);
    }

這個(gè)函數(shù)對(duì)應(yīng)的實(shí)際上就是block中{}塊中的內(nèi)容,通過(guò)block使用的匿名函數(shù)實(shí)際上被作為簡(jiǎn)單的c語(yǔ)言函數(shù)來(lái)處理。這個(gè)函數(shù)的參數(shù)__cself就相當(dāng)于OC里的self,__cself是__main_block_impl_0結(jié)構(gòu)體指針。

總結(jié):

void (^blk)(void) = ^{
        NSLog(@"hello");
    };

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

實(shí)現(xiàn)block,實(shí)際就是在方法中聲明一個(gè)結(jié)構(gòu)體,并且初始化該結(jié)構(gòu)體的成員。
將block語(yǔ)法生成的block賦值給block類型的變量blk,等同于將__main_block_impl_0結(jié)構(gòu)體實(shí)例的指針賦給變量blk。

2.調(diào)用block

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

調(diào)用block就相對(duì)簡(jiǎn)單多了。將第一步生成的block作為參數(shù)傳入FucPtr(也即_main_block_func_0函數(shù)),就能訪問(wèn)block實(shí)現(xiàn)位置的上下文。

自此,block結(jié)構(gòu)總體上分析完了,上面的c代碼看起來(lái)很復(fù)雜,但仔細(xì)讀的話還是很好理解的。
關(guān)于block的數(shù)據(jù)結(jié)構(gòu)runtime是開(kāi)源的。block的數(shù)據(jù)結(jié)構(gòu):

struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};
 
struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

block結(jié)構(gòu)

圖片來(lái)源。這張圖有幾個(gè)要說(shuō)明的地方:
variables:block捕獲的變量,block 能夠訪問(wèn)它外部的局部變量,就是因?yàn)閷⑦@些變量(或變量的地址)復(fù)制到了結(jié)構(gòu)體中。這部分接下來(lái)會(huì)寫到。
而對(duì)于copy和dispose的部分,之后也會(huì)談到。

在objc中,根據(jù)對(duì)象的定義,凡是首地址是isa的結(jié)構(gòu)體指針,都可以認(rèn)為是對(duì)象(id)。這樣在objc中,block實(shí)際上就算是對(duì)象。

2.截獲外部變量

外部變量有四種類型:自動(dòng)變量、靜態(tài)變量、靜態(tài)全局變量、全局變量。我們知道,如果不使用__block 就無(wú)法在block中修改自動(dòng)變量的值。
那么block是怎么截獲外部變量的呢?測(cè)試代碼:
例2:

int a = 1;
static int b = 2;

int main(int argc, const char * argv[]) {

    int c = 3;
    static int d = 4;
    NSMutableString *str = [[NSMutableString alloc]initWithString:@"hello"];
    void (^blk)(void) = ^{
        a++;
        b++;
        d++;
        [str appendString:@"world"];
        NSLog(@"1----------- a = %d,b = %d,c = %d,d = %d,str = %@",a,b,c,d,str);
    };
    
    a++;
    b++;
    c++;
    d++;
str = [[NSMutableString alloc]initWithString:@"haha"];
    NSLog(@"2----------- a = %d,b = %d,c = %d,d = %d,str = %@",a,b,c,d,str);
    blk();
    
    return 0;
}

運(yùn)行結(jié)果:

 2----------- a = 2,b = 3,c = 4,d = 5,str = haha
 1----------- a = 3,b = 4,c = 3,d = 6,str = helloworld

clang轉(zhuǎn)換之后:

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

int a = 1;
static int b = 2;
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *d;
  NSMutableString *str;
  int c;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_d, NSMutableString *_str, int _c, int flags=0) : d(_d), str(_str), c(_c) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *d = __cself->d; // bound by copy
  NSMutableString *str = __cself->str; // bound by copy
  int c = __cself->c; // bound by copy

        a++;
        b++;
        (*d)++;
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_1);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_2,a,b,c,(*d),str);
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->str, (void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main(int argc, const char * argv[]) {
    int c = 3;
    static int d = 4;
    NSMutableString *str = ((NSMutableString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)((NSMutableString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("alloc")), sel_registerName("initWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_0);
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &d, str, c, 570425344));

    a++;
    b++;
    c++;
    d++;
    str = ((NSMutableString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)((NSMutableString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("alloc")), sel_registerName("initWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_3);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_4,a,b,c,d,str);
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}
為了區(qū)別block實(shí)現(xiàn)前后棧上變量的變化,用棧1、棧2來(lái)做區(qū)別

變量a、b是全局的,它們?cè)谌謪^(qū)。變量c、str在函數(shù)棧上,為了區(qū)別在block實(shí)現(xiàn)前、后函數(shù)棧上的變量,下文會(huì)用“棧1”、“棧2”來(lái)區(qū)別。

1.自動(dòng)變量、靜態(tài)變量。
在__main_block_impl_0結(jié)構(gòu)體中可以看到,成員變量多了:

int *d;
  NSMutableString *str;
  int c;

這也是為什么說(shuō)block會(huì)截獲變量。接著看到構(gòu)造函數(shù):

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_d, NSMutableString *_str, int _c, int flags=0) : d(_d), str(_str), c(_c) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }

構(gòu)造函數(shù)中多了int *_d, NSMutableString *_str, int _c三個(gè)參數(shù),并對(duì)對(duì)應(yīng)結(jié)構(gòu)體成員變量進(jìn)行初始化。自此,自動(dòng)變量和靜態(tài)變量被截獲為成員變量。

截獲變量的時(shí)機(jī):
在main函數(shù)的實(shí)現(xiàn)中,

void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &d, str, c, 570425344));

在實(shí)現(xiàn)block時(shí),會(huì)將棧1參數(shù)傳入構(gòu)造函數(shù)中進(jìn)行初始化,所以,block會(huì)在實(shí)現(xiàn)的地方截獲變量,而截獲的變量的值也是實(shí)現(xiàn)時(shí)刻的變量值。另外,如果block語(yǔ)法表達(dá)式中沒(méi)有使用到的靜態(tài)變量、自動(dòng)變量是不會(huì)被追加到__main_block_impl_0結(jié)構(gòu)體中的。

然后我們來(lái)看一下這個(gè)問(wèn)題:為什么在block語(yǔ)法表達(dá)式中不能改變自動(dòng)變量的值,而靜態(tài)變量卻可以呢?從運(yùn)行結(jié)果來(lái)看,為什么block內(nèi)打印的自動(dòng)變量的值沒(méi)有變化?

看到__main_block_func_0函數(shù)的實(shí)現(xiàn):

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int *d = __cself->d; // bound by copy
  NSMutableString *str = __cself->str; // bound by copy
  int c = __cself->c; // bound by copy

        a++;
        b++;
        (*d)++;
        ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_b870bb_mi_1);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_b870bb_mi_2,a,b,c,(*d),str);
    }

為了便于下文的理解,我會(huì)把以下“=”左邊的變量,稱為“臨時(shí)變量”。

  int *d = __cself->d; // bound by copy
  NSMutableString *str = __cself->str; // bound by copy
  int c = __cself->c; // bound by copy
  • 自動(dòng)變量
    測(cè)試代碼中的自動(dòng)變量有兩種:1、基本類型的自動(dòng)變量 int c,2、指向?qū)ο蟮闹羔樀淖詣?dòng)變量 NSMutableString *str。有一個(gè)概念要強(qiáng)調(diào),指針的值是地址。
  struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *d;
  NSMutableString *str;
  int c;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_d, NSMutableString *_str, int _c, int flags=0) : d(_d), str(_str), c(_c) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

分析各種變量之間的關(guān)系:
block截獲自動(dòng)變量為結(jié)構(gòu)體成員變量,對(duì)應(yīng)的數(shù)據(jù)類型是一樣的。
1、在實(shí)現(xiàn)block,調(diào)用__main_block_impl_0構(gòu)造函數(shù)時(shí),棧1自動(dòng)變量的瞬時(shí)值就被截獲、復(fù)制保存到結(jié)構(gòu)體成員變量中初始化。成員變量c得到的是自動(dòng)變量c的值3,成員變量str得到的是自動(dòng)變量str的值(可變字符串對(duì)象1的地址)。
2、在實(shí)現(xiàn)block后、調(diào)用block前,即棧2修改自動(dòng)變量的值,對(duì)結(jié)構(gòu)體中存儲(chǔ)的成員變量的值不會(huì)造成影響。此時(shí),自動(dòng)變量c的值為4,str的值為可變字符串對(duì)象2的地址。
3、調(diào)用block時(shí),即調(diào)用__main_block_func_0函數(shù),此時(shí)函數(shù)中臨時(shí)變量c、str取到的值是結(jié)構(gòu)體中成員變量存儲(chǔ)的值,也即是3和可變字符串對(duì)象1的地址。

如果在block內(nèi)修改自動(dòng)變量的值是可行的,也就相當(dāng)于是在__main_block_func_0函數(shù)中通過(guò)修改臨時(shí)變量的值,來(lái)達(dá)到修改棧上自動(dòng)變量的值的目的。但根據(jù)上面分析,每一步都是值傳遞,所以棧上的自動(dòng)變量的值修改和__main_block_func_0函數(shù)中修改臨時(shí)變量的值互不影響。
OC可能就是基于這一點(diǎn),在編譯層面就防止開(kāi)發(fā)者犯錯(cuò),因此如果在block中修改自動(dòng)變量的值就會(huì)報(bào)錯(cuò)!

如果在block內(nèi)修改自動(dòng)變量的值,那代碼應(yīng)該是這樣的:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSMutableString *str = __cself->str; // bound by copy
  int c = __cself->c; // bound by copy
  
  c++;
  str = ((NSMutableString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)((NSMutableString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("alloc")), sel_registerName("initWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_150b21_mi_3);
    }

雖然在block內(nèi)不能修改str的值,即重新指向其他地址,比如str = [[NSMutableString alloc]init];,但可以在block內(nèi)對(duì)str進(jìn)行操作,比如[str appendString:@"world"];

結(jié)論:block在實(shí)現(xiàn)時(shí)捕獲自動(dòng)變量的瞬時(shí)值。

總結(jié):block捕獲到的變量,都是賦值給block的結(jié)構(gòu)體的,相當(dāng)于const不可改。可以這樣理解block內(nèi)c和str都是const類型。str理解成是常量指針,所以不能修改它指向其他對(duì)象但可以修改它所指向?qū)ο蟮摹爸怠薄?/p>

  • 靜態(tài)變量
    從結(jié)構(gòu)體成員變量int *d;看出,block截獲靜態(tài)變量為結(jié)構(gòu)體成員變量,截獲的是靜態(tài)變量的指針(不是值傳遞了!)。
    調(diào)用block時(shí),即調(diào)用__main_block_func_0函數(shù),此時(shí)函數(shù)中臨時(shí)變量d取到的值是結(jié)構(gòu)體中成員變量存儲(chǔ)的值,即指針,int *d = __cself->d;
    這看起來(lái)似乎和 自動(dòng)變量是指向?qū)ο蟮闹羔?的情況差不多,但一點(diǎn)不同的是,在block內(nèi)修改靜態(tài)變量的值是通過(guò)修改指針?biāo)缸兞?/strong>的來(lái)做的:(*d)++。而這也是為什么block內(nèi)能修改自動(dòng)變量的原因。

2.靜態(tài)全局變量、全局變量。從運(yùn)行結(jié)果來(lái)看,這兩種外部變量的值都在block內(nèi)、外得到增加。因?yàn)樗麄兪侨值模饔糜蚝軓V,所以在block內(nèi)、外都可以訪問(wèn)得到它們。因?yàn)檫@兩種變量都沒(méi)有被追加到__main_block_impl_0結(jié)構(gòu)體中成為成員變量,所以我覺(jué)得它們不算是被捕獲。

分析到這里,相信上面測(cè)試代碼為什么會(huì)得出這樣的運(yùn)行結(jié)果應(yīng)該也能理解了吧?

 2----------- a = 2,b = 3,c = 4,d = 5,str = haha
 1----------- a = 3,b = 4,c = 3,d = 6,str = helloworld

總結(jié):
1.自動(dòng)變量(基本數(shù)據(jù)類型變量、對(duì)象類型的指針變量),可以被block捕獲,但捕獲的是自動(dòng)變量的值。不能在block內(nèi)部改變自動(dòng)變量的值。
2.靜態(tài)變量,可以被block捕獲,捕獲的是變量的地址。通過(guò)使用靜態(tài)變量的指針對(duì)其進(jìn)行訪問(wèn),可以在block內(nèi)改變值。
3.在block內(nèi)沒(méi)有被使用到的自動(dòng)變量、靜態(tài)變量不會(huì)被捕獲。
4.全局變量、靜態(tài)全局變量,因?yàn)樽饔糜蚍秶鷱V,所以可以在block內(nèi)改變它們的值。

現(xiàn)在來(lái)思考一個(gè)問(wèn)題:
靜態(tài)變量可以在block里面直接改變值是通過(guò)傳遞內(nèi)存地址值來(lái)實(shí)現(xiàn)的。那么為什么自動(dòng)變量沒(méi)有使用這種方法呢?
下面看一個(gè)例子:例3

void(^blk_t)();
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
        int i = 1;
        int *a = &i;
        static int j = 2;
        blk_t = ^{
            (*a)++;
            NSLog(@"%d", *a);
        };
    blk_t();
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    blk_t();
}

ARC下運(yùn)行結(jié)果:

2,2
1073741825,2 //點(diǎn)擊

這段代碼說(shuō)明,變量作用域結(jié)束時(shí),該作用域棧上的自動(dòng)變量就被釋放了,因此,不能通過(guò)指針訪問(wèn)原來(lái)的自動(dòng)變量。棧上的變量被釋放掉了,因此點(diǎn)擊屏幕時(shí)訪問(wèn)釋放掉的變量就會(huì)得到意想不到的值。
比如很多時(shí)候,block是作為參數(shù)傳遞供以后回調(diào)用的。往往回調(diào)時(shí),定義變量所在的函數(shù)棧已經(jīng)展開(kāi)了,局部變量已經(jīng)不再棧中了。

插一個(gè)題外話:

void(^blk_t)();
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    {
        int i = 1;
        int *a = &i;
 
        blk_t = ^{
            (*a)++;
            NSLog(@"%d", *a);
        };
    }
    blk_t();
}

本來(lái)例3這段代碼是想這樣寫的,但運(yùn)行結(jié)果很正常。一度很疑惑,以為調(diào)用block時(shí)棧變量沒(méi)有釋放掉。但實(shí)際上它已經(jīng)釋放了,只是它原來(lái)所占的地址還沒(méi)重新被分配給別的變量用,數(shù)據(jù)還是保持原來(lái)的。棧上占用的空間什么時(shí)候被釋放
例3的代碼會(huì)跑出這樣的結(jié)果,猜測(cè)和runloop休眠、喚醒之間釋放自動(dòng)釋放池有關(guān)。

3.block的存儲(chǔ)域以及內(nèi)存管理

3.1存儲(chǔ)域

一般,block有三種:_NSConcreteGlobalBlock、_NSConcreteStackBlock、_NSConcreteMallocBlock,根據(jù)Block對(duì)象創(chuàng)建時(shí)所處數(shù)據(jù)區(qū)不同而進(jìn)行區(qū)別。

_NSConcreteGlobalBlock

是設(shè)置在程序的全局?jǐn)?shù)據(jù)區(qū)域(.data區(qū))中的Block對(duì)象。在全局聲明實(shí)現(xiàn)的block 或者 沒(méi)有用到自動(dòng)變量的block為_(kāi)NSConcreteGlobalBlock,生命周期從創(chuàng)建到應(yīng)用程序結(jié)束。

  • 全局block:

    void (^glo_blk)(void) = ^{
        NSLog(@"global");
    };
    
    int main(int argc, const char * argv[]) {
        glo_blk();
        NSLog(@"%@",[glo_blk class]);
    }
    

    運(yùn)行結(jié)果:

    global
    __NSGlobalBlock__
    

    同時(shí),clang編譯后isa指針為_(kāi)NSConcreteGlobalBlock。

  • 在函數(shù)棧上創(chuàng)建但沒(méi)有截獲自動(dòng)變量

    int glo_a = 1;
    static int sglo_b =2;
    int main(int argc, const char * argv[]) {
        void (^glo_blk1)(void) = ^{//沒(méi)有使用任何外部變量
            NSLog(@"glo_blk1");
        };
        glo_blk1();
        NSLog(@"glo_blk1 : %@",[glo_blk1 class]);
        
        static int c = 3;
        void(^glo_blk2)(void) = ^() {//只用到了靜態(tài)變量、全局變量、靜態(tài)全局變量
            NSLog(@"glo_a = %d,sglo_b = %d,c = %d",glo_a,sglo_b,c);
        };
        glo_blk2();
        NSLog(@"glo_blk2 : %@",[glo_blk2 class]);
    

    運(yùn)行結(jié)果:

    glo_blk1
    glo_blk1 : __NSGlobalBlock__
    glo_a = 1,sglo_b = 2,c = 3
    glo_blk2 : __NSGlobalBlock__
    

    然而,從clang編譯結(jié)果來(lái)看,這兩個(gè)block的isa的指針值都是_NSConcreteStackBlock。

_NSConcreteStackBlock和_NSConcreteMallocBlock

_NSConcreteStackBlock是設(shè)置在棧上的block對(duì)象,生命周期由系統(tǒng)控制的,一旦所屬作用域結(jié)束,就被系統(tǒng)銷毀了。
_NSConcreteMallocBlock是設(shè)置在堆上的block對(duì)象,生命周期由程序員控制的。
稍微改動(dòng)一下例3的代碼:

void(^blk_t)();
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    int i = 1;
    int *a = &i;
    blk_t = ^{
        (*a)++;
        NSLog(@"%d", *a );
    };
    NSLog(@"%@",[blk_t class]);
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    blk_t();
}

運(yùn)行結(jié)果:
ARC:

__NSMallocBlock__
2017-08-11 23:45:52.513 RACPROJECT[49348:1786654] 1073741825

MRC:


運(yùn)行結(jié)果會(huì)根據(jù)ARC\MRC環(huán)境而有所不同。
1.block的類型在ARC下是_NSConcreteMallocBlock,而在MRC下是_NSConcreteStackBlock。在ARC有效時(shí),大多數(shù)情況下編譯器會(huì)恰當(dāng)?shù)嘏袛啵詣?dòng)生成將block從棧上復(fù)制到堆上的代碼。
2.在MRC下,由于Block是_NSConcreteStackBlock類型,它是存在于該函數(shù)的棧幀上的。當(dāng)函數(shù)返回時(shí),函數(shù)的棧幀被銷毀,這個(gè)block的內(nèi)存也會(huì)被清除。因此在點(diǎn)擊屏幕時(shí),程序如圖出現(xiàn)crash。
所以在函數(shù)結(jié)束后仍然需要這個(gè)block時(shí),就必須用copy實(shí)例方法將它拷貝到堆上。這樣即使Block作用域結(jié)束,堆上的Block還可以繼續(xù)使用。

- (void)viewDidLoad {
    [super viewDidLoad];
    int i = 1;
    int *a = &i;
    blk_t = [^{
        (*a)++;
        NSLog(@"%d", *a );
    } copy];
    NSLog(@"%@",[blk_t class]);
}

MRC運(yùn)行結(jié)果:

__NSMallocBlock__

3.2block的自動(dòng)拷貝和手動(dòng)拷貝

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

  • 調(diào)用Block的copy方法
  • 將Block作為函數(shù)返回值時(shí)
  • 將Block賦值給__strong修飾的變量或Block類型成員變量時(shí)
  • 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數(shù)時(shí)

因此ARC環(huán)境下多見(jiàn)的是MallocBlock,但StackBlock也是存在的:
不要進(jìn)行任何copy、賦值等等操作,直接使用block

int main(int argc, const char * argv[]) {
    int val = 1;
    NSLog(@"Stack Block:%@", [^{NSLog(@"Stack Block:%d",val);} class]);
}

運(yùn)行結(jié)果:

Stack Block:__NSStackBlock__

以上四種情況之外,都推薦使用block的copy實(shí)例方法把block復(fù)制到堆上。比如:
block為函數(shù)參數(shù)的時(shí)候,就需要我們手動(dòng)的copy一份到堆上了。這里除去GCD API、系統(tǒng)框架中本身帶usingBlock的方法,其他我們自定義的方法傳遞Block為參數(shù)的時(shí)候都需要手動(dòng)copy一份到堆上。例4:

id getBlockArray()
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            ^{NSLog(@"blk0:%d", val);},
            ^{NSLog(@"blk1:%d", val);}, nil];
}
int main(int argc, char * argv[]) {
    id obj = getBlockArray();
    void (^blk)(void) = [obj objectAtIndex:1];
    blk();
    return 0;
}

運(yùn)行,這段程序崩潰。
在NSArray類的initWithObjects方法上傳遞block參數(shù)不屬于上面系統(tǒng)自動(dòng)復(fù)制的情況(不屬于使用Cocoa框架含有usingBlock的方法傳遞block參數(shù))。通過(guò)之前的分析,顯而易見(jiàn)^{NSLog(@"blk0:%d", val);}是StackBlock,在getBlockArray函數(shù)執(zhí)行結(jié)束時(shí),棧上的block被廢棄,因此在執(zhí)行源代碼的[obj objectAtIndex:1]時(shí),就發(fā)生異常。
解決辦法:手動(dòng)復(fù)制

id getBlockArray()
{
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            [^{NSLog(@"blk0:%d", val) ;} copy],
            [^{NSLog(@"blk1:%d", val);} copy], nil];
}

int main(int argc, char * argv[]) {
    id obj = getBlockArray();
    void (^blk)(void) = [obj objectAtIndex:1];
    blk();
    return 0;
}

最后。ARC會(huì)自動(dòng)處理block的內(nèi)存,不用手動(dòng)release,但MRC下需要,否則會(huì)內(nèi)存泄漏。

3.3block的copy和release

copy

block的復(fù)制可以使用,Block_copy()函數(shù)又或者copy實(shí)例方法。
Block_copy()的實(shí)現(xiàn)。在Block.h文件中看到(在Xcode中也可以找到):

#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))

Block_copy()的原型是_Block_copy()函數(shù),而實(shí)際上最后調(diào)用的是_Block_copy_internal()函數(shù):

//這里傳入的參數(shù)實(shí)際上就是Block
void *_Block_copy(const void *arg) {
    return _Block_copy_internal(arg, WANTS_ONE);
}

static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;

    //1.如果傳遞的參數(shù)為NULL,返回NULL。
    if (!arg) return NULL;
    
    //2.參數(shù)類型轉(zhuǎn)換。轉(zhuǎn)為指向Block_layout結(jié)構(gòu)體的指針。Block_layout結(jié)構(gòu)體請(qǐng)回顧文章開(kāi)頭,相當(dāng)于clang轉(zhuǎn)換后的__main_block_impl_0結(jié)構(gòu)體,包括指向block的實(shí)現(xiàn)功能的指針和各種數(shù)據(jù)。
    aBlock = (struct Block_layout *)arg;

    //3.如果block的flags包含BLOCK_NEEDS_FREE,表明它是堆上的Block(為什么?見(jiàn)第7步注釋)
    //增加引用計(jì)數(shù),返回相同的block
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    //這里刪掉了與垃圾回收(GC)相關(guān)的代碼,GC不做討論

    //4.如果是全局block,什么也不做,返回相同的block
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }

    // Its a stack block.  Make a copy.
    if (!isGC) {
        //5.能夠走到這里,表明是一個(gè)棧Block。需要復(fù)制到堆上。第一步申請(qǐng)內(nèi)存
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return (void *)0;
        //6.將棧數(shù)據(jù)復(fù)制到堆上
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        //7.更新block的flags
        //第一句后面的注釋說(shuō)它不是必須的。
        result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
        //設(shè)置flags為BLOCK_NEEDS_FREE,表明它是一個(gè)堆block。內(nèi)存支持它一旦引用計(jì)數(shù)=0,
        //就進(jìn)行釋放。 “|1”是用來(lái)把block的引用計(jì)數(shù)設(shè)置為1。
        result->flags |= BLOCK_NEEDS_FREE | 1;
        //8.block的isa指針設(shè)置為_(kāi)NSConcreteMallocBlock
        result->isa = _NSConcreteMallocBlock;
        //9.如果block有copy helper函數(shù)就調(diào)用它(和block所持有對(duì)象的內(nèi)存管理有關(guān),文章后面會(huì)講到這部分)
        if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
            //printf("calling block copy helper %p(%p, %p)...\n", aBlock->descriptor->copy, result, aBlock);
            (*aBlock->descriptor->copy)(result, aBlock); // do fixup
        }
        return result;
    }
    else {
        //GC相關(guān)
    }
}

對(duì)_NSConcreteGlobalBlock、_NSConcreteStackBlock、_NSConcreteMallocBlock這三種block,調(diào)用copy方法的總結(jié):


不管block配置在哪里,調(diào)用copy方法進(jìn)行復(fù)制不會(huì)產(chǎn)生任何問(wèn)題。根據(jù)實(shí)際情況需要決定是否調(diào)用copy,如果在所有情況下都進(jìn)行復(fù)制是不可取的做法,這樣會(huì)浪費(fèi)cpu資源。

release

同樣地,block的釋放可以使用Block_release()函數(shù)或者release方法。

#define Block_release(...) _Block_release((const void *)(__VA_ARGS__))

Block_release()原型是_Block_release()函數(shù):

void _Block_release(void *arg) {
    //1.參數(shù)類型轉(zhuǎn)換,轉(zhuǎn)換為一個(gè)指向Block_layout結(jié)構(gòu)體的指針。
    struct Block_layout *aBlock = (struct Block_layout *)arg;
    if (!aBlock) return;

    //2.取出flags中表示引用計(jì)數(shù)的部分,并且對(duì)它遞減。
    int32_t newCount;
    newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK;
    //3.如果引用計(jì)數(shù)>0,表明仍然有對(duì)block的引用,block不需要釋放
    if (newCount > 0) return;

    if (aBlock->flags & BLOCK_IS_GC) {
        //GC相關(guān)
    }
    //4.flags包含BLOCK_NEEDS_FREE(堆block),且引用計(jì)數(shù)=0
    else if (aBlock->flags & BLOCK_NEEDS_FREE) {
        //如果有copy helper函數(shù)就調(diào)用,釋放block捕獲的一些對(duì)象,對(duì)應(yīng)_Block_copy_internal中的第9步
        if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);
        //釋放block
        _Block_deallocator(aBlock);
    }
    //5.全局Block,什么也不做
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        ;
    }
    //6.發(fā)生了一些奇怪的事情導(dǎo)致堆棧block視圖被釋放,打印日志警告開(kāi)發(fā)者
    else {
        printf("Block_release called upon a stack Block: %p, ignored\n", (void *)aBlock);
    }
}

_Block_copy_internal()第9步和_Block_release()第4步中,block所持有對(duì)象的內(nèi)存管理相關(guān)內(nèi)容之后再詳細(xì)說(shuō)明。

4.__block說(shuō)明符

回顧在第二節(jié)中截獲自動(dòng)變量值的例子。block在實(shí)現(xiàn)時(shí)捕獲自動(dòng)變量的瞬時(shí)值,而且不允許在block內(nèi)修改;因?yàn)槌鰲W饔糜蚓蜁?huì)被釋放的原因,也無(wú)法用指針傳遞的方式來(lái)實(shí)現(xiàn)在block內(nèi)修改自動(dòng)變量。

我們知道使用__block 修飾自動(dòng)變量就可以在block內(nèi)改變外部自動(dòng)變量的值。那__block又是怎樣實(shí)現(xiàn)這個(gè)目的的呢?以下分為基本數(shù)據(jù)類型、對(duì)象類型的指針變量來(lái)說(shuō)明。

4.1基本數(shù)據(jù)類型的變量

例5:

int main(int argc, const char * argv[]) {

    __block int c = 3;
    void (^blk)(void) = ^{
        c++;
        NSLog(@"1--- c = %d",c);
    };

    c++;
    NSLog(@"2--- c = %d",c);
    blk();
    NSLog(@"3--- c = %d",c);

    return 0;
}

運(yùn)行結(jié)果:

2--- c = 4
1--- c = 5
3--- c = 5

clang:

// __block為變量c創(chuàng)建的結(jié)構(gòu)體,其中成員c為c的值,forwarding為指向自己的指針
struct __Block_byref_c_0 {
  void *__isa;
__Block_byref_c_0 *__forwarding;
 int __flags;
 int __size;
 int c;
};

// block結(jié)構(gòu)體
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_c_0 *c; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_c_0 *_c, int flags=0) : c(_c->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// block的函數(shù)實(shí)現(xiàn)
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_c_0 *c = __cself->c; // bound by ref

        (c->__forwarding->c)++;
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_944c40_mi_0,(c->__forwarding->c));
    }

//捕獲的變量的copy和release
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->c, (void*)src->c, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->c, 8/*BLOCK_FIELD_IS_BYREF*/);}

//block的描述結(jié)構(gòu)體
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main(int argc, const char * argv[]) {

    __attribute__((__blocks__(byref))) __Block_byref_c_0 c = {(void*)0,(__Block_byref_c_0 *)&c, 0, sizeof(__Block_byref_c_0), 3};
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_c_0 *)&c, 570425344));

    (c.__forwarding->c)++;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_944c40_mi_1,(c.__forwarding->c));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_944c40_mi_2,(c.__forwarding->c));

    return 0;
}

注意到,加了__block修飾的int c變量變成了:__Block_byref_c_0結(jié)構(gòu)體類型的變量

__attribute__((__blocks__(byref))) __Block_byref_c_0 c = {(void*)0,(__Block_byref_c_0 *)&c, 0, sizeof(__Block_byref_c_0), 3};

__main_block_impl_0結(jié)構(gòu)體中c變量不再是int類型了,而是變成了一個(gè)指向__Block_byref_c_0結(jié)構(gòu)體的指針。__Block_byref_c_0結(jié)構(gòu)如下:

struct __Block_byref_c_0 {
  void *__isa;
__Block_byref_c_0 *__forwarding;
 int __flags;
 int __size;
 int c;
};

__Block_byref_c_0結(jié)構(gòu)體的成員變量__forwarding初始化為指向自身的指針。而原本自動(dòng)變量的值3,也成為了結(jié)構(gòu)體中的成員變量。如下__block int c = 3;變成__Block_byref_c_0類型的變量:

__Block_byref_c_0 c = {
  (void*)0,
  (__Block_byref_c_0 *)&c, //指向自己
  0, 
  sizeof(__Block_byref_c_0), 
  3//c的值
};

自動(dòng)變量c加了__block,在clang編譯后變成了一個(gè)結(jié)構(gòu)體__Block_byref_c_0。正是如此,這個(gè)值才能被多個(gè)block共享、并且不受棧幀生命周期的限制。(把__block 變量當(dāng)成是對(duì)象)

看到block的結(jié)構(gòu)體初始化,__Block_byref_c_0類型的變量c以指針形式進(jìn)行傳遞

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

block 捕獲__block變量,捕獲的是對(duì)應(yīng)結(jié)構(gòu)體的變量的地址。

再看一下block執(zhí)行部分的代碼:

__Block_byref_c_0 *c = __cself->c; // bound by ref
(c->__forwarding->c)++;

__Block_byref_c_0 *c = __cself->c;取到指向__Block_byref_c_0結(jié)構(gòu)體類型的變量c的指針。
(c->__forwarding->c)++;然后通過(guò)__forwarding訪問(wèn)到成員變量c,也就是原先的自動(dòng)變量。

那么現(xiàn)在問(wèn)題來(lái)了:
1.block作為回調(diào)執(zhí)行時(shí),局部變量已經(jīng)出棧了,為什么這時(shí)代碼還能正常工作?
2.__forwarding初始化為指向自身的指針,為什么要通過(guò)它來(lái)取得我們要修改的變量而不是c->c直接取出呢?

__block變量的內(nèi)存管理 - copy和release

//dst:目標(biāo)地址 src:源地址
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->c, (void*)src->c, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->c, 8/*BLOCK_FIELD_IS_BYREF*/);}

在上面clang轉(zhuǎn)換的代碼中看到這樣兩個(gè)函數(shù),簡(jiǎn)單來(lái)說(shuō)他們就是用來(lái)做__block的復(fù)制和釋放的,其后中調(diào)用到的_Block_object_assign()函數(shù)和_Block_object_dispose()函數(shù)源碼可以在runtime.c看到。BLOCK_FIELD_IS_BYREF是block截獲__block變量的特殊標(biāo)志。
另外我們也留意到__main_block_desc_0結(jié)構(gòu)體中多了兩個(gè)成員變量:

void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);

上面兩個(gè)函數(shù)以指針形式被賦值到__main_block_desc_0結(jié)構(gòu)體成員變量copy和dispose中。

雖然這兩個(gè)函數(shù)沒(méi)有看到明顯的調(diào)用,但在block從棧復(fù)制到堆上時(shí)以及堆上的Block被廢棄時(shí)會(huì)調(diào)用到這些函數(shù)去處理__block變量(從第3.3節(jié),block的copy函數(shù)源碼第9步和release函數(shù)第4步可知)。

以_Block_object_assign()函數(shù)為例,從上面的源碼截圖中可以得知,實(shí)際上它最后調(diào)用的是_Block_byref_assign_copy()函數(shù)。總結(jié)一下上面截圖函數(shù)所做的事情:

棧block通過(guò)copy復(fù)制到了了堆上。此時(shí),block使用到的__block變量也會(huì)被復(fù)制到堆上并被block持有。如果block已經(jīng)在堆上,再?gòu)?fù)制block也不會(huì)對(duì)所使用的__block有影響。


如果是多個(gè)block使用了同一個(gè)__block變量,那么,有多少個(gè)block被復(fù)制到堆上,堆上的__block變量就被多少個(gè)block持有。當(dāng)__block變量沒(méi)有被任何block持有時(shí)(block被廢棄了),它就會(huì)被釋放。(__block的思考方式和oc的引用計(jì)數(shù)式內(nèi)存管理是相似的,而且__block對(duì)應(yīng)的結(jié)構(gòu)體里也有__isa指針,所以在我看來(lái)也可以把__block變量當(dāng)成對(duì)象來(lái)思考)

棧上__block變量被復(fù)制到堆上后,會(huì)將成員變量__forwarding指針從指向自己換成指向堆上的__block,而堆上__block的__forwarding才是指向自己。


這樣,不管__block變量是在棧上還是在堆上,都可以通過(guò)__forwarding來(lái)訪問(wèn)到變量值。
因此例5代碼中,block內(nèi)的^{c++;};和block外的c++;在clang中轉(zhuǎn)換為如下形式:(c->__forwarding->c)++;
到此,兩個(gè)問(wèn)題都回答了。

總結(jié):
1.block捕獲__block變量,捕獲的是對(duì)應(yīng)結(jié)構(gòu)體的變量的地址。
2.可以把__block當(dāng)做對(duì)象來(lái)看待。當(dāng)block復(fù)制到堆上,block使用到的__block變量也會(huì)被復(fù)制到堆上并被block持有。
至于release的過(guò)程,就相當(dāng)于copy的逆過(guò)程,很好理解就不多說(shuō)了。

block持有對(duì)象

另外,回顧第二節(jié)中的例2,block中使用到(默認(rèn))附有__strong修飾符的NSMutableString類對(duì)象的自動(dòng)變量NSMutableString *str = [[NSMutableString alloc]initWithString:@"hello"];。轉(zhuǎn)換源碼之后,同樣地多了__main_block_copy_0__main_block_dispose_0函數(shù)。

因?yàn)樵贑語(yǔ)言的結(jié)構(gòu)體中,編譯器沒(méi)法很好的進(jìn)行初始化和銷毀操作。這樣對(duì)內(nèi)存管理來(lái)說(shuō)是很不方便的。所以就在 __main_block_desc_0結(jié)構(gòu)體中間增加成員變量 void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*)和void (*dispose)(struct __main_block_impl_0*),利用OC的Runtime進(jìn)行內(nèi)存管理。

與__block相似,對(duì)象類型的指針變量被block截獲值(地址),而block被復(fù)制到堆上后持有這個(gè)對(duì)象,因此,它可以超出作用域而存在。當(dāng)堆上block被廢棄時(shí),釋放block持有的對(duì)象(不是持有變量)。指針指向的對(duì)象并不會(huì)隨block的復(fù)制而復(fù)制到堆上。


_Block_object_assign函數(shù)的調(diào)用相當(dāng)于把對(duì)象retain了,因此block持有對(duì)象。

4.2對(duì)象類型的指針變量

__block NSObject *obj = [[NSObject alloc]init];
    NSLog(@"----%@,%p",obj,&obj);
    void (^blk)(void) = ^{
        NSLog(@"----%@,%p",obj,&obj);
    };
    blk();

clang:

//與__block普通類型變量相比,這個(gè)結(jié)構(gòu)體體多了兩個(gè)成員變量
struct __Block_byref_obj_0 {
  void *__isa;
__Block_byref_obj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);//多出來(lái)的
 void (*__Block_byref_id_object_dispose)(void*);//多出來(lái)的
 NSObject *obj;
};
//這也多出來(lái)的,對(duì)應(yīng)上面的copy函數(shù)指針
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
//多出來(lái),對(duì)飲跟上面的dispose函數(shù)指針
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
//余下的部分基本和__block普通類型變量差不多
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_obj_0 *obj; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_obj_0 *_obj, int flags=0) : obj(_obj->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_obj_0 *obj = __cself->obj; // bound by ref

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_a45e66_mi_1,(obj->__forwarding->obj),&(obj->__forwarding->obj));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main(int argc, const char * argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_obj_0 obj = {(void*)0,(__Block_byref_obj_0 *)&obj, 33554432, sizeof(__Block_byref_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))};
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_7__3g67htjj4816xmx7ltbp2ntc0000gn_T_main_a45e66_mi_0,(obj.__forwarding->obj),&(obj.__forwarding->obj));
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_obj_0 *)&obj, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}
__block NSObject *obj = [[NSObject alloc]init];
相當(dāng)于
__block __strong NSObject *obj = [[NSObject alloc]init];

可以看到,和4.1節(jié)一樣,block 捕獲__block變量,捕獲的是對(duì)應(yīng)結(jié)構(gòu)體的變量的地址。并且當(dāng)block從棧復(fù)制到堆上,__block變量從棧復(fù)制到堆,且堆__block變量持有賦值給它的對(duì)象。當(dāng)__block變量被廢棄時(shí),釋放賦值給__block變量的對(duì)象。

持有關(guān)系:堆Block -> 堆__block變量 -> 對(duì)象
只要堆上的__block變量存在,對(duì)象就繼續(xù)處于被持有的狀態(tài)。

總結(jié)一下以上4個(gè)章節(jié):

  • 捕獲持有是兩個(gè)概念,不要混淆。(持有是MRC下的說(shuō)法,而在ARC下的內(nèi)存管理我們談的是“強(qiáng)弱指針引用”。)
  • block相當(dāng)于是對(duì)象。
  • 能夠被block捕獲的變量:自動(dòng)變量、靜態(tài)變量、__block變量。block捕獲:自動(dòng)變量的值(基本數(shù)據(jù)類型-值,對(duì)象類型指針-對(duì)象地址);靜態(tài)變量的地址;__block變量則是其對(duì)應(yīng)結(jié)構(gòu)體變量的指針:地址。
  • 自動(dòng)變量是值傳遞,所以不能在block內(nèi)改變值。
  • __block變量和靜態(tài)變量是地址傳遞,可以在block內(nèi)直接改變值。
  • 全局變量、靜態(tài)全局變量,因?yàn)樽饔糜蚍秶鷱V,所以可以在block內(nèi)改變它們的值
  • 為了解決block所在變量域結(jié)束后block仍然可用的問(wèn)題,需要把棧block復(fù)制到堆上
  • ARC時(shí),在四種情況下stackBlock會(huì)自動(dòng)復(fù)制到堆上,其余時(shí)候必須手動(dòng)copy才會(huì)復(fù)制到堆上;而MRC則不會(huì),只有手動(dòng)copy才會(huì)復(fù)制到堆上
  • __block變量也可以當(dāng)成是對(duì)象看待。block復(fù)制到堆上時(shí),它使用到的__block變量也會(huì)復(fù)制到堆上,無(wú)論MRC還是ARC。
  • block復(fù)制到堆上引起的持有對(duì)象的關(guān)系:“->”代表“持有”
對(duì)象類型變量:堆Block -> 對(duì)象
__block 普通基本數(shù)據(jù)類型變量:堆Block -> 堆__block變量
__block __strong 對(duì)象類型變量: 堆Block -> 堆__block變量 -> 對(duì)象
對(duì)象本身就在堆區(qū),不存在復(fù)制不復(fù)制的說(shuō)法,只是它被“持有”的數(shù)量有所增加
  • 在ARC下,__block會(huì)導(dǎo)致對(duì)象被retain。而在MRC下不會(huì)。

5.循環(huán)引用

循環(huán)引用是什么其實(shí)很多人應(yīng)該都知道,這里簡(jiǎn)單提一下。比如說(shuō):
1.多個(gè)對(duì)象之間相互引用形成環(huán)。A對(duì)象強(qiáng)引用B,B強(qiáng)引用A,于是兩者內(nèi)存一直無(wú)法釋放。
2.對(duì)象自己引用自己。

例6:

#import <Foundation/Foundation.h>
typedef void (^PersonBlock)(void);
@interface Person : NSObject
@property (nonatomic ,assign) NSInteger age;
@property (nonatomic ,strong) NSString *name;
- (void)configurePersonBlock:(PersonBlock)blk_t;
@end

#import "Person.h"
@interface Person()
//不作為公有屬性,而是在對(duì)外方法接口中把Block傳進(jìn)來(lái)
@property (nonatomic ,strong) PersonBlock blk;
@end

@implementation Person
- (void)configurePersonBlock:(PersonBlock)blk_t{
    self.blk = blk_t;
}

- (void)actionComplete{
    self.blk();
}
@end
#import "ViewController.h"
#import "BViewController.h"

@interface ViewController ()
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    UIButton *btn = [[UIButton alloc]initWithFrame:CGRectMake(50, 50, 50, 50)];
    btn.backgroundColor = [UIColor redColor];
    [self.view addSubview:btn];
    [btn addTarget:self action:@selector(click:) forControlEvents:UIControlEventTouchUpInside];
}

- (void)click:(id)sender {
    BViewController *bVC = [[BViewController alloc]init];
    [self.navigationController pushViewController:bVC animated:YES];
}
@end
--------------------------------------------------------------------
#import "BViewController.h"
#import "Person.h"
@interface BViewController ()
@property (nonatomic ,strong) Person *person;
@property (nonatomic ,copy) NSString *str;
@end

@implementation BViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.str = @"haha";
    
    self.person = [[Person alloc]init];
    self.person.name = @"commet";
    self.person.age = 18;
    [self.person configurePersonBlock:^{
        NSLog(@"printf str:%@",self.str);
    }];
    [self.person actionComplete];
}
@end
1.多個(gè)對(duì)象之間相互引用形成環(huán)。

成環(huán):B控制器通過(guò)strong實(shí)例變量持有person對(duì)象,person持有block,block又持有self(即B控制器)。

block用到的外部的對(duì)象,mallocBlock會(huì)在內(nèi)部持有它。
Block捕獲了實(shí)例變量_var,那么也會(huì)自動(dòng)把self變量一起捕獲了,因?yàn)閷?shí)例變量是與self所指代的實(shí)例相關(guān)聯(lián)在一起的。但是像例6這樣寫:[self.person configurePersonBlock:^{ NSLog(@"%ld",_var); }];由于沒(méi)有明確使用self變量,所以很容易就會(huì)忘記self也被捕獲了。而直接訪問(wèn)實(shí)例變量和通過(guò)self來(lái)訪問(wèn)是等效的,所以通常屬性來(lái)訪問(wèn)實(shí)例變量,這樣就明確地使用了self了。
self也是對(duì)象,所以block捕獲它的時(shí)候也會(huì)持有該對(duì)象。

例7:

#import "ViewController.h"
#import "Person.h"

@interface ViewController ()
@property (nonatomic ,strong) Person *person;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person1 = [[Person alloc]init];
    person1.name = @"commet";
    person1.age = 18;
    [person1 configurePersonBlock:^{
        NSLog(@"%@",person1.name);
    }];
}
@end
2.自己引用自己

5.1解除循環(huán)引用

以例6為例分析:



例6的引用環(huán)是這樣的,只要打破其中一道引用,就能解除循環(huán)引用。

  • 解除①引用
    可以這么修改:
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.str = @"haha";
    
    self.person = [[Person alloc]init];
    self.person.name = @"commet";
    self.person.age = 18;
    [self.person configurePersonBlock:^{
        NSLog(@"printf str:%@",self.str);
        self.person = nil;//改了這里
    }];
    [self.person actionComplete];
}
B控制器push沒(méi)有發(fā)生內(nèi)存泄漏

ps:必須執(zhí)行block才能解除①的引用。

  • 解除②引用
    在Person類中:
@implementation Person

- (void)configurePersonBlock:(PersonBlock)blk_t{
    self.blk = blk_t;
}

- (void)actionComplete{
    self.blk();
    self.blk = nil;//改了這句
}

然后在控制器中調(diào)用它:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.str = @"haha";
    
    self.person = [[Person alloc]init];
    self.person.name = @"commet";
    self.person.age = 18;
    [self.person configurePersonBlock:^{
        NSLog(@"printf str:%@",self.str);
    }];
    [self.person actionComplete];
}
B控制器push還是沒(méi)有發(fā)生內(nèi)存泄漏

但是前面這兩種做法又并不是那么合理,因?yàn)樗麄兌紡?qiáng)迫調(diào)用actionComplete這個(gè)方法來(lái)解除其中一層引用,但有時(shí)候你無(wú)法假定調(diào)用者一定會(huì)這么做。

  • 解除③引用
    block要使用的外部變量,作為block形參傳遞進(jìn)block。
Person類
#import <Foundation/Foundation.h>
typedef void (^PersonBlock)(NSString *);

@interface Person : NSObject
@property (nonatomic ,assign) NSInteger age;
@property (nonatomic ,strong) NSString *name;

- (void)configurePersonBlock:(PersonBlock)blk_t;

- (void)actionComplete:(NSString *)str;
@end

#import "Person.h"
@interface Person()
@property (nonatomic ,strong) PersonBlock blk;
@end

@implementation Person

- (void)configurePersonBlock:(PersonBlock)blk_t{
    self.blk = blk_t;
}

- (void)actionComplete:(NSString *)str{
    self.blk(str);
}
@end
----------------------------------------------------------------

#import "BViewController.h"
#import "Person.h"
@interface BViewController ()
@property (nonatomic ,strong) Person *person;
@property (nonatomic ,copy) NSString *str;
@end

@implementation BViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.str = @"haha";
    
    self.person = [[Person alloc]init];
    self.person.name = @"commet";
    self.person.age = 18;
    [self.person configurePersonBlock:^(NSString *str) {
        NSLog(@"printf str:%@",str);
    }];
    [self.person actionComplete:self.str];

}
@end
B控制器push依舊沒(méi)有發(fā)生內(nèi)存泄漏

這種方法存在一個(gè)缺點(diǎn),就是如果在block中要使用到很多外部變量、對(duì)象,那么就要給Block添加很多參數(shù)。

往往我們使用__weak來(lái)打破這種強(qiáng)引用。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    self.str = @"haha";
    
    self.person = [[Person alloc]init];
    self.person.name = @"commet";
    self.person.age = 18;
    
    __weak typeof(self) weakself = self;
    [self.person configurePersonBlock:^ {
        NSLog(@"printf str:%@",weakself.str);
    }];
    [self.person actionComplete];

}

但也不是說(shuō)在block中就一定要使用weakself,因?yàn)橛袝r(shí)候循環(huán)引用未必存在:
比如說(shuō)Masonry,一般我們是這樣寫的:

[_view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.size.mas_equalTo(CGSizeMake(60, 60));
        make.right.equalTo(self.view.mas_right).offset(-24);
        make.bottom.equalTo(self.view.mas_bottom).offset(-50);
    }];

顯然block引用了self,但這樣寫并沒(méi)有引起循環(huán)引用:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

在mas_makeConstraints這個(gè)方法中,可以看到self并沒(méi)有強(qiáng)引用block,而這個(gè)block只是作為參數(shù)傳遞進(jìn)來(lái)并直接調(diào)用而已。

說(shuō)完weakself那么不得不提起strongself了。Apple 官方文檔有講到,如果在 Block 執(zhí)行完成之前,self 被釋放了,weakSelf 也會(huì)變?yōu)?nil。比如:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
        
    Person *person = [[Person alloc]init];
    person.name = @"commet";
    person.age = 18;
    
    __weak typeof(person) weakPerson = person;
    [person configurePersonBlock:^ {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"printf str:%@",weakPerson.name);
        });
    }];
    [person actionComplete];
}

運(yùn)行結(jié)果:

printf str:(null)

[person actionComplete];調(diào)用block之后,viewDidLoad方法作用域結(jié)束后,person對(duì)象被釋放。由于dispatch_after的延遲執(zhí)行,在Block執(zhí)行完成前,捕獲的對(duì)象釋放了,block捕獲weakPerson變?yōu)閚il。

由于weakself無(wú)法控制對(duì)象釋放時(shí)機(jī)所帶來(lái)的問(wèn)題,我們?cè)贐lock中使用__strong修飾weakself保證任何情況下self在超出作用域后仍能夠使用,防止self的提前釋放。

__weak typeof(person) weakPerson = person;
    [person configurePersonBlock:^ {
        __strong typeof(weakPerson) strongPerson = weakPerson;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"printf str:%@",strongPerson.name);
        });
    }];
    [person actionComplete];

當(dāng)block執(zhí)行完畢就會(huì)釋放自動(dòng)變量strongSelf,釋放對(duì)self的強(qiáng)引用。
所以總結(jié)來(lái)說(shuō),weakself是用來(lái)解決block循環(huán)引用的問(wèn)題的,而strongself是用來(lái)解決在block執(zhí)行過(guò)程中self提前釋放的問(wèn)題。

最后還有一種解除循環(huán)引用的方法:使用__block變量
修改一下例7:

- (void)viewDidLoad {
    [super viewDidLoad];

    Person *person1 = [[Person alloc]init];
    person1.name = @"commet";
    person1.age = 18;
    
    __block Person *blkPerson = person1;
    
    [person1 configurePersonBlock:^{
        NSLog(@"%@",blkPerson.name);
        blkPerson = nil;
    }];
    person1.blk();
}


這段代碼沒(méi)有引起循環(huán)引用,但是如果沒(méi)有執(zhí)行賦值給成員變量的blk的block(即刪掉person1.blk();這句),就會(huì)造成循環(huán)引用引起內(nèi)存泄漏。person持有block,block持有__block變量,__block變量又持有person對(duì)象,于是就形成了保留環(huán)...

雖然使用__block可以控制對(duì)象的持有時(shí)間,在執(zhí)行block時(shí)可以動(dòng)態(tài)地決定是否將nil或者其他對(duì)象賦值在__block變量中,但它有一個(gè)缺點(diǎn)就是,必須執(zhí)行一次block才能打破循環(huán)引用。

ps:在ARC下__block會(huì)導(dǎo)致對(duì)象被retain,有可能導(dǎo)致循環(huán)引用。而在MRC下,則不會(huì)retain這個(gè)對(duì)象,也不會(huì)導(dǎo)致循環(huán)引用。

參考文檔:
Block_private.h
runtime.c
文章:
A look inside blocks: Episode 3 (Block_copy)
objc 中的 block
談Objective-C block的實(shí)現(xiàn)
Block 小測(cè)驗(yàn)
深入研究Block用weakSelf、strongSelf、@weakify、@strongify解決循環(huán)引用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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