Block 理解

本文将按照如下顺序逐个说明

  1. Block 的本质
  2. Block 捕获成员变量
  3. Block 的存储域以及内存生命周期分析
  4. Block 捕获成员变量的生命周期
  5. GCD 里面的 Block 分析
  6. MRC & ARC 下 Block 内存管理区别
  7. Block **weak 分析 / **strongSelf 分析
  8. 一些常问的关于 Block 的面试题回答

本文不关注 Block 的语法,只尝试说明 Block 捕获变量的方式,Block 捕获的变量的生命周期以及 Block 自身的生命周期。最后对一些常见的自己的疑问进行一些梳理归纳。

# Block 的本质

使用 clang -rewrite-objc main.m 编译如下代码

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

得到结果是

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;
  }
};
//---
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
//---
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
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    //执行 block 对应方法
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}
//---
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_9w_q4lvthyn17v0cxxm5s7fsb500000gn_T_main_aef28d_mi_0);
}

从上面编译后的文件里可以看到一些信息block实现的主体就是__main_block_impl_0,这个结构体里面嵌套了一个名字为__block_impl的结构体以及名为__main_block_desc_0的结构体指针。闭包里面的代码部分被抽出来生成了一个 C 语言的函数实现 __main_block_func_0,注意的是这个函数的参数就是结构体本身,这么做有啥用呢?方法实现里根本没用到 block 实例啊,后面会用到的...

说明一下这个 __block_impl 结构体:

  • __block_impl结构体里面有void *isa成员变量,很眼熟对不对,这个就是Objective-C对象实现中的对象指针,所以我们通常认为block也算是Objective-C对象。
  • __main_block_impl_0初始化方法impl.isa = &_NSConcreteStackBlock; Demo 里的 block 类型属于 _NSConcreteStackBlock
  • __block_impl结构体里有函数指针 FuncPtr,在 __main_block_impl_0 初始化的时候指向block闭包里面函数的实现。

# Block 捕获变量

# Block 捕获普通类型自动变量

Block 的一个强大之处在于能捕获变量,在执行闭包方法的时候利用外部捕获的变量得到一些想要的结果,这是怎么做到的?将 demo 稍作改动,添加一个自动变量,如下:

int main(int argc, const char * argv[]) {
    int a = 0;
    void(^block)(void) = ^() { NSLog(@"hello world a = %d",a); };
    a = 3;
    block();
    return 0;
}
//执行结果如下
hello world a = 0;

继续使用 clang -rewrite-objc main.m 进行编译,得到如下结果(已精简)

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int a = __cself->a; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_9w_q4lvthyn17v0cxxm5s7fsb500000gn_T_main_473c35_mi_0,a);
}
int main(int argc, const char * argv[]) {
    int a = 0;
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    a = 3;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

这次跟没有捕获成员变量时候比,在 __main_block_impl_0 结构体里多了一个成员变量 a,结合 main.m 实现以及 __main_block_impl_0() 的初始化方法可以看到这个成员变量 a 的初始化是依靠外部同名自动变量 a 的赋值,所以打印的时候会打印初始化 block 时候外部变量的 a 的值。如果在 block 初始化之后再次对外部自动变量 a 进行了修改,则 block 自动变量不会被修改。参考上面 demo 的执行结果。

# Block 捕获并修改普通类型自动变量/__block 修饰符分析

有一个值得思考的问题,如果我想要在 block 的闭包函数里修改外部变量的值该怎么做?目前的实现只是打印了 block 成员变量a的值,如果修改的话,我们也仅仅只能修改成员变量a的值,而没有途径修改外部变量的值。怎么办?一个方法就是在 block 内部做一个指针,指向外部自动变量 a,这样就能在执行方法的时候通过指针去修改外部变量的值。怎么样才能实现我们这个方案呢,block 提供了一个修饰符 __block 去修饰外部成员变量,当 block 捕获了带有 __block 修饰的外部成员变量的时候会自动在结构体内部生成一个引用外部变量的指针变量。如下 demo 所示:

int main(int argc, const char * argv[]) {
    __block int a = 0; ①
    void(^block)(void) = ^() { NSLog(@"hello world a = %d",a); ②};
    ③
    a = 3;
    block();
    return 0;
}
//执行结果是
hello world a = 3

注:①②③ 处分别打印 a 的内存地址,在 ARC/MRC 下会有不同的结果,在 MRC 下内存地址打印都是一样的,但是在 ARC 下,① 和 ②③ 打印的地址不一样,① 打印的是栈上地址,②③ 打印的是堆上的地址,原因是在 ARC 下向 block 赋值的时候,会自动将 block 从栈上拷贝到堆上。这个现象和后面说到的像 strong 修饰符修饰的 block 属性赋值是一个道理。

编译后的结果为

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_9w_q4lvthyn17v0cxxm5s7fsb500000gn_T_main_bd6f8c_mi_0,(a->__forwarding->a));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->a, 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_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 0};
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
    (a.__forwarding->a) = 3;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}

根据编译后的结果来看,block 捕获了带有 __block 的自动变量之后生成的代码变的很多,而且出现了很多新的方法,比如 __main_block_dispose_0__main_block_copy_0。同时 block 对应的结构体 __main_block_impl_0 里多出来一项 __Block_byref_a_0 *a;,这个跟我们之前预想的并不一样,我们之前预想的就是多出来一个 int *a 应该就好了,但是这里多出了一个 __Block_byref_a_0 结构体类型的指针,来看一下定义 :

struct __Block_byref_a_0 {
  void *__isa;
  __Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

包含 void *__isa 成员变量,__Block_byref_a_0 这个也算是一个对象类型,里面包含了一个 int a 的成员变量,以及一个指向自己的指针。 被 __block 修饰符修饰的成员变量被编译器转化为 __Block_byref_a_0 类型的变量。初始化的时候将外部的 __Block_byref_a_0 变量地址赋值给 block 结构体成员变量里的 __Block_byref_a_0 指针。在 block 闭包函数执行的时候通过这个指针来去修改外部变量的值。

但是有个问题,为啥不直接生成一个 int *a 这样的指针,而去生成一个那么麻烦的类型呢?后面详细说明

思考一下,为什么block捕获带有 __block 修饰符的时候,闭包执行永远输出外部变量a的当前值?

# Block 捕获对象

先上个 demo

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

编译后

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSString *a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSString *_a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//
int main(int argc, const char * argv[]) {
    NSString * a = (NSString *)&__NSConstantStringImpl__var_folders_r4_93dvjwh16d17_brzl2bv_xb40000gn_T_main_0c7a66_mi_0;
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, 570425344));
    a = (NSString *)&__NSConstantStringImpl__var_folders_r4_93dvjwh16d17_brzl2bv_xb40000gn_T_main_0c7a66_mi_2;
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
}
//
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSString *a = __cself->a; // bound by copy
 NSLog((NSString *)&__NSConstantStringImpl__var_folders_r4_93dvjwh16d17_brzl2bv_xb40000gn_T_main_0c7a66_mi_1,a);
}

block实现的结构体__main_block_impl_0里多了一个 NSString *a,貌似和捕获正常的自动变量没什么差别。从 mian.m 方法里的 block 初始化过程里可以看到,这个结构体成员变量的具体的赋值过程其实就是外部字符串对象 a 直接赋值给了 block 结构体成员变量 a,具体的 block 闭包实现中也是直接从 block 结构体中取出 a 直接进行的访问。所以其实闭包里面可以修改对象指针的话也丝毫不会影响外部的同名自动变量 a(这俩根本就不是一个对象指针...),但是如果直接在里面修改的话,编译器是不支持的,会报错(跟捕获普通自动变量时候报一个错)如下:

Error:Variable is not assignable (missing __block type specifier)

但是,如果不是对捕获的外部变量进行指针修改的话,是可以对这些外部变量进行一些操作的,比如说我们捕获的变量是一个可变数组,demo 如下:

int main(int argc, const char * argv[]) {
    NSMutableArray *muarr = [NSMutableArray arrayWithCapacity:0];
    void(^block)(void) = ^() {
        [muarr addObject:@"1"];
        NSLog(@"muarr = %@",muarr);
    };
    block();
    return 0;
}
//执行结果如下:
BlockDemo[1088:31899] muarr = (
    1
)

可以看到我们在不改变外部对象指针的情况下,对外部对象进行了操作。

写到这儿不知道为啥想起来,其实跟字符串对象用 strong 修饰还是 copy 修饰有异曲同工之妙了,一般面试题问起来的话,字符串都是用 copy 修饰的,原因是如果字符串 NSString 类型指向了一个 NSMutableString 类型,那当可变字符串的字符更改的时候,看起来就是虽然 NSString 指针没变,但是内容发生了改变,会引起使用上的一些矛盾之处。所以尽量会使用 copy 修饰符修饰。

同理,想要修改对象指针的话,还是要加上 __block 修饰符。可以自己使用 clang -rewrite-objc main.m 的方式生成一下源代码分析一下。需要注意在 block 闭包实现的时候对变量访问的方式。

看编译后的代码被捕获的__block变量是在栈上__Block_byref_fan_0 object,初始化Block对象的时候使用object->forwarding指针去初始化Block内部对应生成的 __Block_byref_fan_0 *指针,而object->forwarding这个指针是指向object自身的内存地址。这块儿的分析有助于我们后面对__block修饰的变量内存进行分析。

# Block 的存储域以及内存生命周期分析

# Block 存储域

Block 存储域分为三种类型:栈上(_NSConcreteStackBlock),堆上(_NSConcreteMallocBlock)和数据区(_NSConcreteGlobalBlock)。上面的例子中Block都是存在栈上的,和普通的变量一样声明在函数外的话Block就是存在数据区中,比如下面这种 case:

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

Block 就是存储在数据区,clang -rewrite-objc main.m 之后生成的代码中,Block 初始化方法中有这样一行 impl.isa = &_NSConcreteGlobalBlock;,说明 Block 所属的类是 _NSConcreteGlobalBlock

值得分析的是,Block 存储域存在堆上的情况,<OC 高级编程> 一书举了如下的例子

typedef int(^blk_t)(int);
blk_t func(int rate) {
    return ^(int count) { return rate *count; };
}
int main(int argc, const char * argv[]) {
    blk_t blk = func(5);
    NSLog(@"%d",blk(10));
    return 0;
}

func()方法中Block作为了返回值返回,按道理来讲 Block 超出其函数作用域之后就会被销毁,但是在 ARC 环境下这个 main.m 能正常输出执行结果。当我们修改 main.m 文件的编译选项为 -fno-objc-arc 的时候编译器会报错:

Error"Returning block that lives on the local stack

说明是 ARC 下编译器帮我们做了一些事情,避免了 block 在栈上不能返回的问题。将非 ARC 下的代码修改为

blk_t func(int rate) {
    return [^(int count) { return rate *count; } copy];
}

才能避免编译失败问题,并正确得到执行结果。

书上给出的在 ARC 下func方法编译完后大致的代码如下

blk_t func(int rate) {
   blk_t tmp = ((int (*)(int))&__func_block_impl_0((void *)__func_block_func_0, &__func_block_desc_0_DATA, rate));
   tmp = objc_retainBlock(tmp);
   return objc_autoreleaseReturnValue(tmp);
}

即 ARC 下编译器对本身是存在于栈上的Block对象执行了一次copy操作,转移到堆上。

运行时库中 NSObject.mm 文件中可以看到objc_retainBlock就是_Block_copy方法。

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

大部分情况下编译器都会处理将栈上的Block复制到堆上的 case.

# Block 内存分析

存在栈上的Block,如果所属的作用域结束,该Block就被废弃,由于__block修饰的变量也在栈上那么__block修饰的变量也会被废弃。

使用栈来存储变量的优点是内存是自动为你管理的。你无需手动分配内存,或者在你不再需要时释放内存。参考地址 (opens new window)

刚才说过在 ARC 下,编译器会自动将 Block 从栈上复制到堆上,使用的方法为 _Block_copy 下面是官方的的 API 说明

// Create a heap based copy of a Block or simply add a reference to an existing one. This must be paired with Block_release to recover memory, even when running under Objective-C Garbage Collection.
BLOCK_EXPORT void *_Block_copy(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
//Lose the reference, and if heap based and last reference, recover the memory
BLOCK_EXPORT void _Block_release(const void *aBlock)
    __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

即使用 _Block_copy 可以创建基于堆上的一份儿Block的复制,同时_Block_copy一定是配合_Block_release使用,否则会造成内存泄漏。

# Block 捕获成员变量的生命周期

# Block 捕获普通对象的生命周期分析

捕获普通自动变量对象 A 的话,Block 会影响普通自动变量的生命周期,即自动变量作用域结束,A 并不会被立即释放。而是跟捕获了它的 Block 的生命周期同步。原因是为了捕获自动变量 A,Block 对象内部会生成一个捕获对象的同类型的对象 A' 来对捕获对象进行持有,即使得 A 的引用计数加一。所以当 A 的作用域结束之后 A 依然能存活,直到 Block 被释放,A 跟着一起被释放。

int main(int argc, const char * argv[]) {
    blk_t blk;
    {
        Fan *fan = [[Fan alloc] init];
        blk = ^() {
            NSLog(@"a = %@",fan);
        };
    }
    blk();
    return 0;
}

我们编译完的这部分代码中包含如下代码,这是为了管理__block结构体中捕获的对应外部变量的fan的内存而生。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Fan *fan;
  ...
};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->fan, (void*)src->fan, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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

编译后的代码中这两个方法并没有被调用,而是当Block被从栈上拷到堆上,以及在堆上被释放的时候才会被调用。如图:

但是实践中很奇怪的一点是在 ARC 环境下,对Block对象进行了 copy 操作还是没能触发_Block_object_assign这个方法的执行,但是在 MRC 环境下同样条件就能触发_Block_object_assign这个方法。这里 ARC 编译后的代码应该是做了一些额外的操作。

# Block 捕获带有 __block 修饰符对象的声明周期分析

如果被__block修饰的对象是自动变量,而且Block并没有执行从栈拷贝到堆上的操作,那和普通的自动变量对象一样并没有什么区别,__block修饰的对象也是跟着Block对象一起释放。

但是实际情况里__block修饰对象可能存在栈上,可能存在堆上,而Block对象也有可能从栈上被复制到堆上,这些场景下,__block修饰对象的生命周期是什么样的?

Block捕获带有__block修饰符的自动变量的时候,编译后代码出现下面的一些源码,即 Block对象除了管理自己的内存之外还要额外操心__block修饰变量的内存了。

这里 __block变量对象内存管理和普通的引用计数管理思想基本一致了,即哪个Block对象持有了__block变量,则该Block对象有义务对__block变量进行释放。当所有Block都被释放的时候__block变量也跟着一起释放掉。

# GCD 里面的 Block 分析

GCD 提供的 API 里大量使用了 Block 作为参数,比如我们常用的 API dispatch_async(queue, ^(void){}) 等,通常来说,我们并不需要过分关注 API 中 Block 的内存管理。因为 GCD 会自动帮我们处理,参考 dispatch_async API 的官方说明(其实 dispatch_after API 里也是这么说的)

dispatch_async 参数 block 的解释 The block to submit to the target dispatch queue. This function performs Block_copy and Block_release on behalf of callers. This parameter cannot be NULL. 即这个方法会自动在合适的时机执行 Block_CopyBlock_release 两个方法。这样就保证了在执行 Block 之前不会因为 Block 的作用域的原因而是的 Block 提前被释放,将 Block 放到堆上是比较安全的做法。

现在有个问题,Block 被谁持有了?通过写 demo 打符号断点 _Block_copy,跟到了其上一步调用 _dispatch_Block_copy ,然后在 GCD 的 queue.c 源码 (opens new window)中找到了答案,是一个 dispatch_block_t 的类型的变量持有了 Block 对象。

关于 GCD 里面的 Block 里面是否应该使用 self,YYKit 的一个 issue (opens new window) 里讨论的比较火热。但是很多评论都是有问题的,包括 YY 的理解都是有问题的,YY 对 block 的理解就是「self->_queue->block->self 这不是循环引用吗」但是根据我们刚才的分析,其实 GCD 的 queue 并没有持有 block,GCD 的 Block 内存管理跟当前执行所在的类没有任何关系,系统负责Block_CopyBlock_release,我理解这种 case 算不上循环引用。所以可以放心的在 GCD 里面使用 self,而不需要 weak dance.

Block_Copy 内部的实现机制可以参考这篇文章 (opens new window)

还有一个 GCD 中应该注意的内存问题,即 ARC 和 dispatch queues 以及 GCD Block 内存管理之间的关系。 需要分类讨论

  1. If your deployment target is lower than iOS 6.0 or Mac OS X 10.8

    You need to use dispatch_retain and dispatch_release on your queue. ARC does not manage them.

  2. If your deployment target is iOS 6.0 or Mac OS X 10.8 or later

    ARC will manage your queue for you. You do not need to (and cannot) use dispatch_retain or dispatch_release if ARC is enabled.

参考地址 - Does ARC support dispatch queues? (opens new window)

在 MRC 下 GCD 也会自动执行 Block_CopyBlock_Release 方法,所以在 MRC 下的 GCD Block 里面继续使用 self 也不会产生内存的问题。

总结就是在古老的系统中,即使编译器开启了 ARC 也不一定能管理 dispatch_object 对象,还好我们现在早已经过了兼容的那个阶段。

# MRC & ARC 下 Block 内存管理区别

MRC 和 ARC 下 Block 内存管理的区别主要在于 MRC 下并不会对Block进行主动 copy 操作。举个例子:

blk returnblk() {
    int a = 0;
    blk tempblk = ^(){
        NSLog(@"hello world,a = %d",a);
    };
    return tempblk;
}
int main(int argc, const char * argv[]) {
    blk newblk = returnblk();
    newblk();  ①
    return 0;
}

MRC 下在 ① 的位置打断点,观察 newblk 的类型为 __NSStackBlock__,同样断点 ARC 下观察到 newblk 的类型为 __NSMallocBlock__。说明在 ARC 下 tempblk 在返回的时候自动执行了一次 copy 操作。

还有一个更经典的例子

@interface Fan : NSObject {
    dispatch_queue_t queue;
    Blk blk;
}
@end
- (instancetype)init {
    self = [super init];
    if (self) {
        blk = ^() {NSLog(@"self %@",self);};
        ①
    }
    return self;
}

MRC 下在 ① 处打断点,blk 的类型为 __NSStackBlock__,同样断点 ARC 下观察到 blk 的类型为 __NSMallocBlock__。ARC 自动在给成员变量 blk 赋值的时候进行了一次 copy 操作。上面这个例子还引出了一个循环引用的问题,我们下面说。

# MRC 下 retain 方法使用注意

MRC 下还需要注意的是,如果Block在栈上的话,对Block进行 retain 操作没有任何意义。必须对其进行 copy 操作才能将其从栈上复制到堆上。如果Block在堆上的话,对其进行 retain 操作,Block对象的引用计数会加一。

在对 Block 对象进行 copy 操作的时候,Block_copy 方法和 copy 方法执行的效果是一样的,同理,Block_releaserelease 方法效果也一样。

# MRC 下破解循环引用的方式

对于 MRC 下,为了防止循环引用,我们使用__block来修饰在 Block 中使用的对象。

原因是在当 Block 被从栈拷贝到堆上的时候,不会对带有__block修饰符的自动变量对象进行 retain 操作,不带有 __block修饰符的自动变量对象会被 retain 操作。 所以上面的例子需要现用 __block id temp = self; 也对 self 进行一次引用,然后在 block 里面使用 temp.

对于 ARC 下,为了防止循环引用,我们使用__weak来修饰在 Block 中使用的对象。

# Block **weak 分析 / **strongSelf 分析

关于 Block weak 和 strong 的说明,这两个配合使用存在的意义就是让 block 避免循环引用。举个例子,在视图控制器 (VC) 的 viewDidLoad 方法里执行如下代码 ,FRButton 内部持有了这个点击 block

__weak typeof(self) weakSelf = self;
[button setClickBlk:^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"strongSelf = %@",strongSelf);
    });
}];

当 dismiss 的时候,VC 并没有被释放,而是等 dispatch_after 的 block 执行之后 VC 才被释放。 分析在 button 赋值 block 对象时候 __weak__strong 的用法,通过引用计数和持有关系进行内存分析。

  1. 首先在 button block 的外部使用 __weakself 进行持有,并没有增加 self 的引用计数。
  2. 在 button block 内部进行对 weakSelf 进行 __strong 修饰符的 strongSelf 持有,增加了 self 的引用计数
  3. 分析一下引用关系啊, VC 持有了 button,button 内部持有了 blockblock 捕获的是 VC 的 weakSelf。 Block 里面的 strongSelf 仅仅是个自动变量而已(看上面 clang 编译出来的代码就知道,strongSelf 应该是在编译后的闭包函数对应的内联方法里声明的),不用分析其引用关系。但是 strongSelf 增加了 VC 的引用计数。
  4. 当点击之后,然后用另外的方法让 VC 迅速(3 秒) dismiss,这个时候 VC 并没有被释放,因为 strongSelf 增加了 VC 的引用计数。而 dispatch_after 的 block 持有了 strongSelf,dispatch_after 的 block 在延时 3 秒后执行,执行完之后 dispatch_after 的 strongSelf 也被释放(自动变量作用域结束自动被回收),这个时候 VC 的引用计数变为 0 才会被释放。此时 weakSelf 被置为了 nil,第 3 步里面的引用关系断掉了,即 Block 不再持有 VC 了。所以不会出现循环引用的问题。

释放顺序是:1、 VC; 2、Button;3、Block。

误区一Block捕获__weak修饰符的对象,虽然我们不能通过执行 clang -rewrite-objc BlockDemo/main.m 来看最后的编译结果(会报错,`cannot create __weak reference because the current deployment target does

  not support weak references`),但我猜原理是`Block`对象结构体内部直接生成一个 `__weak`修饰的成员变量指向`__weak`修饰的对象,而不是生成`__block`修饰的那种结构来,这样才能保证`Block`不会对`self`进行强持有来增加引用计数。

误区二,另外一个之前分析时候陷入误区的点是,总会纠结block里面的代码会不会执行,其实执行与否都不会影响当前这种结构的的内存分析,执行匿名函数代码的话strongSelf到最后会被释放,block跟着一起释放。不执行的话self其实也没有强持有block,所以不会造成内存引用问题。

正是因为误区二,我们引出另外一个问题,即在下面的 ① 的位置添加了一个判断,有必要吗?

__weak typeof(self) weakSelf = self;
[networkManager fetchFinishBlk:^(response){
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (nil == strongSelf) {  ①
        return;
    }
    [strongSelf xxx];
}];

之前总是觉得 strongSelf 会强引用 self,所以 self 在执行 block 闭包函数之前不会被释放,其实有可能在执行 block 之前,self 已经被释放掉了,则 weakSelfnil, 这个时候 strongSelf 还没有来得及增加的引用计数呢... 所以加上这个判断是必要的。

那又引出一个问题,如果我一定想要执行 block 闭包函数里的方法呢,可以参考这篇文章 (opens new window)里的做法,大体思路是先触发循环引用,然后在 block 执行完毕之后再将循环引用破解掉。

再进一步思考,如果是下面这种情况,会造成循环引用吗?

__weak typeof(self) weakSelf = self;
[cell setEditPressedBlk:^{
    __strong typeof(weakSelf) strongSelf = weakSelf; ①
    [strongSelf pickerCancel];
    strongSelf.textFiledView = [TFAlertView Title:@"Title" message:@"" complete:^(NSString *text, TFAlertView *alertview) {
        [strongSelf modifyNameWithValue:text];  ②
    }];
}];

# 一些常见的关于 Block 的面试题

Q: Block 作为对象的属性应该用 copy 修饰还是 strong 修饰?? A: 在 MRC 下的话,必须用 copy 修饰,用 retain 修饰的话,栈上的 Block 无法被拷贝对堆上,导致使用的时候可能出问题。 在 ARC 下用 strong 即可,当给 block 属性赋值的时候会自动将栈上的 block 拷贝到堆上,用 copy 的话效果是一样的,但是苹果的官方文档还是建议我们即使是在 ARC 上也使用 copy 修饰符,因为 copy 会显式地说明我们对 block 的操作。同时,我们的工程偶尔也会看到给 block 属性进行赋值的时候,手动添加了一个 copy 方法,比如 self.block = [blk copy]; 其实是完全没有必要的。 官方地址 - Objects Use Properties to Keep Track of Blocks (opens new window)

#参考地址# WWDC_712 (opens new window) Blocks Programming Topic iOS 中的 block 是如何持有对象的 (opens new window) 深入研究 Block 捕获外部变量和__block 实现原理 (opens new window) A look inside blocks: Episode 3 (Block_copy) (opens new window) OC 高级编程学习总结之 GCD (opens new window) weak 与block 修饰符到底有什么区别 (opens new window) Block 在 ARC 和 MRC 下的使用分析 (opens new window) 深入研究 Block 用 weakSelf、strongSelf、@weakify、@strongify 解决循环引用 (opens new window) 循环引用的破局法门 (opens new window) 深入分析 Objective-C block、weakself、strongself 实现原理 (opens new window)

最后送上两个关于 block 的语法说明

  1. https://weibo.com/1765732340/GfRtraOYj?type=comment
  2. http://fuckingblocksyntax.com