Block

深入学习Block。

Block

Block的概要

  1. Block功能:带有自动变量(局部变量)的匿名函数(不带名称的函数)。Blocks 是闭包在 OC 语言中的实现,并不是 iOS 独有的概念,在 C++、Java 等语言也有实现闭包,只是名称不同而已。在objc中,block实际上就算是对象。

  2. Block语法:

    1
    2
    3
    ^void(int event){
    print("buttonId:%d event = %d\n",i,event);
    }

    特点:a.没有函数名 b.带有“^”号
    ^ 返回值类型 参数列表 表达式

  3. 优势

    可代替 Delegate 完成回调,而不需要像 Delegate 那样繁琐
    在某些方面,可代替 selector(如 NSNotificationCenter 在 addObserver 的时候,可以使用 block,而不用单独定义方法)
    延长对象的生命周期(Block 会自动持有对象)
    提高代码的复用性和可读性
    常用于:View 动画、GCD、网络异步请求
    

Block模式—block特性

截获自动变量值

“带有自动变量值”在Block中表现为“截获自动变量值”

1> 对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的. 也就是说block的自动变量截获只针对block内部使用的自动变量, 不使用则不截获, 因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大.

1
2
3
4
5
6
int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();

输出为
age = 10

2> 对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的.

1
2
3
4
5
6
__block int age = 10;
myBlock block = ^{
NSLog(@"age = %d", age);
};
age = 18;
block();

输出为
age = 18

第一种情况block内部不允许修改变量的值, 第二种情况下可以. (有例外, 静态变量, 静态全局变量, 全局变量即使不使用block修饰符也可以在block内部修改其值)
附有
block说明符的自动变量可在Block中赋值,该变量成为__block变量。

截获对象

对象不同于自动变量, 就算对象不加上block修饰符, 在block内部能够修改对象的属性.
block截获对象与截获自动变量有所不同.
堆块会持有对象, 而不会持有
block修饰的对象, 而栈块永远不会持有对象, 为什么呢?

堆块作用域不同于栈块, 堆块可以超出其作用域地方使用, 所以堆块结构体内部会保留对象的强指针, 保证堆块在生命周期结束之前都能访问对象. 而对于__block对象为什么不会持有呢? 原因很简单, 因为__block对象会跟随block被复制到堆中, block再去引用堆中的__对象(后面会讲这个过程)..
栈块只能在当前作用域下使用, 所以其内部不会持有对象. 因为不存在在作用域之外访问对象的可能(栈离开当前作用域立马被销毁)

Block的实现

Block的实质

Blocks 的数据结构

对应的结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};

struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};

通过该图,我们可以知道,一个 Block 实例实际上由 6 部分构成:

isa 指针: 所有对象都有该指针,用于实现对象相关的功能

flags: 用于按 bit 位表示一些 block 的附加信息,本文后面介绍 block copy 的实现代码可以看到对该变量的使用;

reserved: 保留变量;

invoke: 函数指针,指向具体的 block 实现的函数调用地址;

descriptor: 表示该 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函数的指针;

variables: capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中;
copy : 用于保留捕获的对象
dispose : 用于释放捕获的对象

Objective-C 中的 Stack 和 Heap

首先所有的 Objective-C 对象都是分配在 Heap(堆) 的。 在 OC 最典型的内存分配与初始化就是这样的:

NSObject *obj = [[NSObject alloc] init];

一个对象在 alloc 的时候,就在 Heap 分配了内存空间。

Stack 对象通常有速度的优势,而且不会发生内存泄露问题。那么为什么 OC 的对象都是分配在 Heap 的呢? 原因在于:

Stack 对象的生命周期所导致的问题。例如一旦函数返回,则所在的 Stack Frame(栈帧)就会被销毁。那么此时返回的对象也会一并销毁。这个时候我们去 retain 这个对象是无效的。因为整个 Stack Frame 都已经被销毁了。简单而言,就是 Stack 对象的生命周期不适合 OC 的引用计数内存管理方法。

Stack 对象不够灵活,不具备足够的扩展性。创建时长度已经是固定的,而stack对象的拥有者也就是所在的 Stack Frame。

Block 存储域

block中的isa指向的是该block的Class。在block runtime中

全局块(_NSConcreteGlobalBlock)程序的数据区域(.data区)
栈块(_NSConcreteStackBlock)
堆块(_NSConcreteMallocBlock)

全局块存在于全局内存中, 相当于单例.
栈块存在于栈内存中, 超出其作用域则马上被销毁
堆块存在于堆内存中, 是一个带引用计数的对象, 需要自行管理其内存

当struct第一次被创建时,它是存在于该函数的栈帧上的,其Class是固定的_NSConcreteStackBlock。其捕获的变量是会赋值到结构体的成员上,所以当block初始化完成后,捕获到的变量不能更改。

当函数返回时,函数的栈帧被销毁,这个block的内存也会被清除。所以在函数结束后仍然需要这个block时,就必须用Block_copy()方法将它拷贝到堆上。这个方法的核心动作很简单:申请内存,将栈数据复制过去,将Class改一下,最后向捕获到的对象发送retain,增加block的引用计数。详细代码可以直接点这里查看。

全局块(_NSConcreteGlobalBlock)

block定义在全局变量的地方
block没有截获任何自动变量

以上两个情况满足任意一个则该block为全局块, 全局块的生命周期贯穿整个程序, 相当于单例.全局的静态 Block,不会访问任何外部变量。
简单地讲,如果一个block钟没有引用外部变量并且没有被其他对象持有,就是NSConcreteGlobalBlock。NSConcreteGlobalBlock是全局的block,在编译期间就已经决定了,如同宏一样。

栈块(_NSConcreteStackBlock)
保存在栈中的 Block,当函数返回时会被销毁。(ARC 中系统实现了自动 copy, 将创建在栈上的 Block 自动拷贝到堆上,所以不存在此类型的 Block)NSConcreteStackBlock就是引用了外部变量的block,

NSConcreteGlobalBlock由于处在 data段,可以通过指针安全访问。StackBlock处在内存栈区,如果其变量作用域结束, 这个 block 就被废弃,block 上的block变量也同样会被废弃。为了解决这个问题,block 提供了 copy 的功能,将block和block 变量从栈拷贝到堆。当 block 从栈拷贝到堆后,当栈上变量作用域结束时,仍然可以继续使用 block。堆上的 block 类型为 _NSConcreteMallocBlock,会将 _NSConcreteMallocBlock 写入 isa。

堆块(_NSConcreteMallocBlock)
栈块copy之后就变成堆块,一个block被copy时,将生成NSConcreteMallocBlock(block没有retain)

只要这个NSConcreteMallocBlock存在,内部对象的引用计数就会+1。

NSConcreteStackBlock处于内存的栈区,ARC中大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上。当block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法。但方法/函数在内部已经实现了一份拷贝了 block 参数的代码,或者如果编译器自动拷贝,那么调用者就不需再手动拷贝。

__block变量的存储域

默认block捕获到的变量,都是赋值给block的结构体的,相当于const不可改。为了让block能访问并修改外部变量,需要加上__block修饰词。

block的copy操作究竟做了什么呢?

由上图可知, 对一个栈块进行copy操作会连同block与block变量(不管有没有使用)在内一同copy到堆上, 并且block会持有block变量(使用).
ps : 堆上的block及__block变量均为对象, 都有各自的引用计数

当然, 当block被销毁时, block持有的__block也会被释放

到这里我们能知道, 此思考方式与Objective-C的引用计数内存管理完全相同.

那么有人就会问了, 既然block变量也被复制到堆上去了, 那么访问该变量是访问栈上的还是堆上的呢?? forwarding 终于要闪亮登场了.

通过forwarding, 无论实在block中, block外访问block变量, 也不管该变量在栈上或堆上, 都能顺利地访问同一个__block变量.

当 block 从栈被拷贝到堆时,forwarding 指针变量也会指向堆区的结构体。但是为什么要这么做呢?为什么要让原本指向栈区的结构体的指针,去指向堆区的结构体呢?看起来匪夷所思,实则原因很简单,要从 forwarding 产生的缘由说起。想想起初为什么要给 block 添加 copy 的功能,就是因为 block 获取了局部变量,当要在其他地方(超出局部变量作用范围)使用这个 block 的时候,由于访问局部变量异常,导致程序崩溃。为了解决这个问题,就给 block 添加了 copy 功能。在将 block 拷贝到堆上的同时,将 forwarding 指针指向堆上结构体。后面如果要想使用 block 变量,只要通过 __forwarding 访问堆上变量,就不会出现程序崩溃了。。

block 的自动拷贝和手动拷贝

那么Blocks提供的复制方法究竟是什么?

ARC有效的时候,大多数轻型下编译器会恰当的进行判断,自动生成将Block从栈上复制到堆上的代码。

block 的自动拷贝

ARC下, 以下几种情况下, 编译器会帮我们把栈上的block复制到堆中

block作为函数返回值返回时。编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;

将block赋值给__strong修饰符id类型或block类型成员变量时。编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递block时。这些方法会在内部对传递进来的 block 调用 copy 或 _Block_copy 进行拷贝;

block 的手动拷贝

此外的情况需要手动对block调用copy方法

把block作为函数/方法的参数传入时才需要对block进行copy操作.

我们对不同地方的block调用copy会产生什么效果呢?

Block 循环引用

如果在 Block 中使用附有 __strong 修饰符的对象类型自动变量,那么当 Block 从栈复制到堆时,该对象为 Block 所持有,于是便导致了循环引用的产生。
self 持有 Block,Block 持有 self,这正是循环引用。

比较

下面对使用 block 变量避免循环引用的方法和使用 weak 修饰符及 __unsafe_unretained 修饰符避免循环引用的方法做个比较。

使用 __block 变量的优点如下:

通过 __block 变量可控制对象的持有期间
在不能使用 __weak 修饰符的环境中不使用 __unsafe_unretained 修饰符即可(不必担心 悬垂指针 )
在执行 Block 时可动态地决定是否将 nil 或其他对象赋值在 __block 变量中。

使用 __block 变量的缺点如下:

为避免循环引用必须执行 Block
存在执行了 Block 语法,却不执行 Block 的路径时,无法避免循环引用。若由于 Block 引发了循环引用时,根据 Block 的用途选择使用 __block 变量、 __weak 修饰符或 __unsafe_unretained 修饰符来避免循环引用。
MRC下用__block可以避免循环引用(原因见上面block特性之截获自动变量值)
ARC下用__weak来避免循环引用

这里需要提醒大家的是, 只有堆块(_NSConcreteMallocBlock)才可能会造成循环引用, 其他两种block不会

Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用

weakSelf 是为了block不持有self,避免Retain Circle循环引用。在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf。

strongSelf的目的是因为一旦进入block执行,假设不允许self在这个执行过程中释放,就需要加入strongSelf。block执行完后这个strongSelf 会自动释放,没有不会存在循环引用问题。如果在 Block 内需要多次 访问 self,则需要使用 strongSelf。

AFN经典说起,以下是AFN其中的一段代码:

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
#pragma mark - NSOperation

- (void)setCompletionBlock:(void (^)(void))block {
[self.lock lock];
if (!block) {
[super setCompletionBlock:nil];
} else {
__weak __typeof(self)weakSelf = self;
[super setCompletionBlock:^ {
__strong __typeof(weakSelf)strongSelf = weakSelf;
dispatch_group_t group = strongSelf.completionGroup ?: url_request_operation_completion_group();
dispatch_queue_t queue = strongSelf.completionQueue ?: dispatch_get_main_queue();
#pragma clang diagnostic pop

dispatch_group_async(group, queue, ^{
block();
});

dispatch_group_notify(group, url_request_operation_completion_queue(), ^{
[strongSelf setCompletionBlock:nil];
});
}];
}
[self.lock unlock];
}

如果block里面不加strong typeof(weakSelf)strongSelf = weakSelf会如何呢?就会输出null。
weakSelf之后,无法控制什么时候会被释放,为了保证在block内不会被释放,需要添加__strong。

在block里面使用的__strong修饰的weakSelf是为了在函数生命周期中防止self提前释放。strongSelf是一个自动变量当block执行完毕就会释放自动变量strongSelf不会对self进行一直进行强引用

@weakify(self) 和 @strongify(self) 就是比我们日常写的weakSelf、strongSelf多了一个@autoreleasepool{}而已.

要点总结

Block 执行的代码其实在编译的时候就已经准备好了
本身 Block 就是一个普通的 OC 对象。正因为它是对象,Block 可以被作为参数传递,可以作为返回值从一个方法返回,可以用来给变量赋值
__block 修饰符在 MRC 下不会进行引用计数加 1,而 ARC 下则会加 1
对于 Block 外的变量引用,Block 默认是将其复制到其数据结构中来实现访问的
对于用 __block 修饰的外部变量引用,Block 是复制其引用地址来实现访问的

面试题

  1. 讲讲block;

  2. block在ARC中和传统的MRC中的行为和用法有没有什么区别,需要注意些什么?
    注意:1>block的内存管理 (block的实现是基于指针和函数指针)

    • 如果没有copy操作,block代码默认放在栈内存(弱引用)。
    • 如果有copy操作,block升级放在堆内存(强引用)。
      2>防止循环引用
      解决:非ARC(MRC):__block
      ARC:__weak或者__unsafe_unretained

      补充:__block__weak修饰符的区别
      __block 不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型。
      __weak只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int)。
      __block 对象可以在block中被重新赋值,__weak不可以。

  1. block和self的循环引用;到底是如何循环引用的;block的代码实现;为什么会造成循环引用;block是如何强引用self的;

    一个对象中强引用了block,在block中又强引用了该对象,就会发射循环引用。
    解决方法是将该对象使用weak或者block修饰符修饰之后再在block中使用。

id weak weakSelf = self; 或者 weak typeof(&*self)weakSelf = self该方法可以设置宏
id
block weakSelf = self;
或者将其中一方强制制空 xxx = nil。

检测代码中是否存在循环引用问题,可使用 Facebook 开源的一个检测工具 FBRetainCycleDetector 。

  1. block为什么要修饰为copy;
    因为block默认是在栈区,作为成员变量使用时要保证拷贝到堆区,虽然说是copy,实际上也只是浅拷贝,还是通过引用计数控制释放。

  2. 有哪几种类型的block;什么情况下block会从栈区复制到堆区;
    三种。在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上,只有当block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法;

  3. NSMutableArray在block中修改时,是否要修饰为__block;

  1. block和delegate你更倾向于用哪个?为什么?
    Block.
    block使代码更紧凑,便于阅读,delegate可以设置必选和可选的方法实现.
    block可以访存局部变量. 不需要像以前的回调一样,把在操作后所有需要用到的数据封装成特定的数据结构, 你完全可以直接访问局部变量.

  2. block在MRC下如何解决循环引用

  1. block循环引用了如何解决?如何解除循环引用;
  2. Block在栈中还是堆中?全局变量是在栈还是在堆中?成员变量呢?block分配在哪里;
  3. block的原理是什么,如何去找到这个block;(函数指针)
  1. block在传递的时候,是否会改变存储位置?比如是否会从栈复制到堆;原来的block是否会被释放;

  2. Block的实现内部机制,

  3. Block中的循环引用;Block的复制;Block的存储位置;Block如何改变外面的变量;__block修饰符的内部机制!!!;

  4. Block的嵌套使用;

  5. [※※]在block内如何修改block外部变量?外部的变量是怎么传到Block中去的;

Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。

  1. [※※※]使用系统的某些block api(如UIView的block版本写动画时),是否也考虑引用循环问题?
    UIView的block版本写动画时不需要考虑。

系统的某些block api中,UIView的block版本写动画时不需要考虑,但也有一些api 需要考虑:

所谓“引用循环”是指双向的强引用,所以那些“单向的强引用”(block 强引用 self )没有问题,
但如果你使用一些参数中可能含有 ivar 的系统 api ,如 GCD 、NSNotificationCenter就要小心一点:比如GCD 内部如果引用了 self,而且 GCD 的其他参数是 ivar,则要考虑到循环引用:

参考:
<http://blog.devtang.com/2013/07/28/a-look-inside-blocks/ >
http://www.jianshu.com/p/abeb5848b57a#
http://www.jianshu.com/p/e03292674e60#
http://www.jianshu.com/p/e03292674e60#

深入研究Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用https://halfrost.com/ios_block_retain_circle/

block没那么难(一):block的实现https://www.zybuluo.com/MicroCai/note/51116

Blockhttp://www.samirchen.com/block-in-objc/

              

文章目录
  1. 1. Block
    1. 1.1. Block的概要
    2. 1.2. Block模式—block特性
      1. 1.2.1. 截获自动变量值
      2. 1.2.2. 截获对象
    3. 1.3. Block的实现
      1. 1.3.1. Block的实质
      2. 1.3.2. Block 存储域
      3. 1.3.3. __block变量的存储域
      4. 1.3.4. block 的自动拷贝和手动拷贝
        1. 1.3.4.1. block 的自动拷贝
        2. 1.3.4.2. block 的手动拷贝
      5. 1.3.5. Block 循环引用
      6. 1.3.6. Block用weakSelf、strongSelf、@weakify、@strongify解决循环引用
    4. 1.4. 要点总结
  2. 2. 面试题
本站总访问量 本站访客数人次 ,