IOS Block 之我见

引子

昨日午后至今日,一直在研究IOS 中 Block原理和机制,妄图一下午就看清,实则有“急功近利”之嫌。不过后来也想明白了,技术原理需要细细揣摩和反复钻研,才有味道。先就这两日的研究做一小结。

本文内容:

  • Block类型
  • Capture机制
  • ARC和MRC下Block的不同
  • Block 实践中的循环引用问题

block简介

都说block是闭包的实现,那么什么是闭包?闭包是函数块,以及其运行的上下文环境,脱离创造他的环境也可以运行。

在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。 from wiki

闭包是可以包含自由(未绑定到特定对象)变量的代码块. from baidu

block 类型

包括三种:

  1. NSConcreteGlobalBlock 全局静态block,不访问(捕获)任何外部变量的block 位于静态区。不用担心内存管理问题
  2. NSConcreteStackBlock 保存在栈中的Block, 对外部变量有访问,当函数返回时被销毁
  3. NSConcreteMallocBlock 保存在堆中的Block,和一般的NSObject管理方式一样,当引用计数为0时销毁

需要注意:ARC、MRC 下三种block 均存在:

1、StackBlock 在执行copy之后,会变为MallocBlock 保存在堆中。

2、但是在ARC 下 将一个捕获了外部变量的Block绑定到一个strong变量时,编译器自动将其copy,因此常见MallocBlock和GlobeBlock,

3、有图为证

block 捕获原理

简单的说,block 捕获的外部变量 ,将其转化为Block 自身Struct 的成员变量, 这就解释了为何block中不能修改外部变量的原因。但加上_block修饰符,即捕获外部变量的指针,因此可以修改其内容。

最后附张图显示block数据结构

捕获的原理代码:学会使用clang -rewrite-objc main.m命令,查看编译后的c++源码

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main() {
int a = 100;
void (^block2)(void) = ^{
printf("%d\n", a);
};
block2();

return 0;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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
printf("%d\n", a);
}

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 a = 100;
void (*block2)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);
((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);

return 0;
}

个人理解

最后说一下自己对block 循环应用的理解

1、当block作为对象的成员属性存在时,block 中又引用了(实际上是捕获)该对象,就会造成循环引用。因为block 捕获时自动将其retain
此时需要使用__weak修饰符 ,在block 捕获前进行weak。同时,严谨期间,为了避免过早释放,可以在block中定义新的strong变量引用该对象,这样也起到retain该对象的左右,但是block中执行完毕后回释放调该新的strong引用,达到有效避免循环引用的情况。

2、其他情况下大可放心,block的生命周期将由ARC管理,会及时释放。

其他情况参见此处

参考资料

1、唐巧的博客

2、hherima

3、Matt Galloway