Fork me on GitHub

iOS 中的 block

  在入行做 iOS 开发没多久的时候,感觉 block 挺神秘的,当时在跟同事交流的时候一直称呼它为 代码块,想当然的把它理解成为是一个匿名函数。但是随着技术能力的增长,以及大神们在博客中无私的分享,对 block 渐渐有了更加深入的了解,也算是走出了误区。


什么是 Block,Block 的本质是什么?

  block 是封装了函数调用以及函数调用环境的 OC 对象,派生自 NSBlock,它内部也有 isa 指针。


Block捕获变量

  为了保证block内部能够正常访问外部的变量,block有个变量捕获机制。

image-20190312120502166

为什么局部变量需要捕获?

  考虑作用域的问题,需要跨函数访问,就需要捕获。

block里访问self是否会捕获?

  会,self是当调用block函数的参数,参数是局部变量,self指向调用者。

block里访问成员变量是否会捕获?

  会,成员变量的访问其实是self->xx,先捕获self,再通过self访问里面的成员变量。

block对auto和static变量捕获有什么差异?

  auto自动变量可能会销毁的,内存可能会消失,不采用指针访问;static变量一直保存在内存中,指针访问即可。【auto变量block访问方式是值传递(如果是普通类型,则进行值拷贝,如果是对象类型,会使其引用计数加1),static变量block访问方式是指针传递】

block对全局变量的捕获方式是?

  block不需要对全局变量捕获,都是直接采用取值的。


Block的类型

block有哪几种类型?

  block的类型,取决于isa指针,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型。

  • NSGlobalBlock ( _NSConcreteGlobalBlock ) 在数据区
  • NSStackBlock ( _NSConcreteStackBlock ) 在栈区
  • NSMallocBlock ( _NSConcreteMallocBlock ) 在堆区

iOS 的内存布局 由地地址向高地址 依次为:

  1. 代码区(.text):存放代码二进制文件。

  2. 数据区(.data):存放变量数据。

  3. 堆区:由程序员申请并释放(直接或间接调用 alloc 函数开辟内存)。

  4. 栈区:普通局部变量,由系统管理释放(编译时已确定),一般是出了作用域自动释放。

如何判断 block 是哪种类型?

  • 没有访问 auto 变量的 block 是 __NSGlobalBlock__ ,放在数据段(全局变量)。
  • 访问了 auto 变量的 block 是 __NSStackBlock__
  • [__NSStackBlock__ copy] 操作就变成了 __NSMallocBlock__

这里有一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int age = 1;
void (^block1)(void) = ^{
NSLog(@"block1");
};

void (^block2)(void) = ^{
NSLog(@"block2: %d", age);
};

NSLog(@"%@/%@/%@", [block1 class], [block2 class], [^{
NSLog(@"block3: %d", age);
} class]);
// 输出: __NSGlobalBlock__/__NSMallocBlock__/__NSStackBlock__

NSLog(@"代码段:%p", (IMP)main);
// 输出: 代码段:0x103b20140

NSLog(@"全局区:%p, 堆区:%p, 栈区:%p", block1, block2, ^{NSLog(@"block3: %d", age);});
// 输出: 全局区:0x103b23138, 堆区:0x6000006c4000, 栈区:0x7ffeec0dee30

  通过上面的示例,也验证了前面的说法。

  但是有一个问题,示例代码中的 block2 和 block3 应该是一样的才对啊,为什么 block2 在堆区,而 block3 在栈区呢?

  原来,在 ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上

在 ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上的几种情况?

  1. block 作为函数返回值时

  2. 将 block 赋值给 __strong 指针时

  3. block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时

  4. block 作为 GCD API 的方法参数时

  为了验证第二点,我们特地写个 Demo 来看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int age = 1;
void (^block1)(void) = ^{
NSLog(@"block1");
};

void (^block2)(void) = ^{
NSLog(@"block2: %d", age);
};

NSLog(@"%@/%@/%@", [block1 class], [block2 class], [^{
NSLog(@"block3: %d", age);
} class]);

NSLog(@"代码段:%p", (IMP)main);
NSLog(@"全局区:%p, 堆区:%p, 栈区:%p", block1, block2, ^{NSLog(@"block3: %d", age);});

  从 Demo 中可以看出,block2 和 block3 的区别就在于 block2 有一个 __strong 的指针引用,正因为此,block2 的内存地址与 block3 完全不在同一个区间。

  博客原文作者也注明了使用 block 作为属性时,应该使用什么关键词进行描述。

MRC 下 block 属性的建议写法
@property (copy, nonatomic) void (^block)(void);

ARC 下 block 属性的建议写法
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

  这里应该才参考第二点,将 block 赋值给 __strong 指针时,其实已经将 block 拷贝到堆区了,再用 copy 或者是 strong 都是对其引用计数 +1,所以 ARC 下并没有严格的限制。如果大家在使用过程中遇到了什么问题,欢迎一起讨论一下。

对每种类型 block 调用 copy 操作后是什么结果?

  • __NSGlobalBlock__ 调用copy操作后,什么也不做。
  • __NSStackBlock__ 调用copy操作后:从栈复制到堆;副本存储位置是
  • __NSMallocBlock__ 调用copy操作后:引用计数增加;副本存储位置是

对象类型的 auto 变量

无论 MRC 还是 ARC,栈空间上的 block,不会持有对象;堆空间的 block,会持有对象。

当 block 内部访问了对象类型的 auto 变量时,是否会强引用?

答案:分情况讨论,分为栈 block 和堆 block

栈 block
a) 如果 block 是在栈上,将不会对 auto 变量产生强引用
b) 栈上的 block 随时会被销毁,也没必要去强引用其他对象

堆 block

  1. 如果 block 被拷贝到堆上:
    a) 会调用 block 内部的 copy 函数
    b) copy 函数内部会调用 _Block_object_assign 函数
    c) _Block_object_assign 函数会根据 auto 变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

  2. 如果 block 从堆上移除
    a) 会调用 block 内部的 dispose 函数
    b) dispose 函数内部会调用 _Block_object_dispose 函数
    c) _Block_object_dispose 函数会自动释放引用的 auto 变量(release)

正确答案:

  • 如果 block 在空间,不管外部变量是强引用还是弱引用,block 都会弱引用访问对象。
  • 如果 block 在空间,如果外部强引用,block 内部也是强引用;如果外部弱引用,block 内部也是弱引用。

这里有一个疑问:

  上面的结论是根据别人博客内容以及理论推导的,但是我自己进行 Demo 演练的时候,却发现栈区的 block 似乎也会对 auto 类型的对象进行强引用。

1
2
3
4
5
6
7
8
Person *p = [Person new];
NSLog(@"Person: %p, retain count: %ld", p, (long)CFGetRetainCount((__bridge CFTypeRef)(p)));
__weak void(^weakblock)(void) = ^{
NSLog(@"%p", p);
};
NSLog(@"栈区 block: %p", weakblock);
weakblock();
NSLog(@"Person: %p, retain count: %ld", p, (long)CFGetRetainCount((__bridge CFTypeRef)(p)));

  这里 block 引用了 auto 变量,同时没有触发拷贝到堆空间的条件,实际打印出来的内存地址也确实在栈区。但是 auto 类型的对象 p,打印结果显示,block 引用后,p 的引用计数确实增加了1,不知道是我的理解有误,还是这个结论是不正确的,希望以后能够解开这个谜团,也希望大神看到后能给一些指导。


参考文章

  本文只是从本人的一些疑惑点来讨论 block 的一些知识点。参考原文中对 Block 的剖析更加详细,大家可以认真研读一下。

传送门:iOS-Block本质

------------- 本文结束感谢您的阅读 -------------