block

作者: 意一ineyee | 来源:发表于2018-08-20 18:22 被阅读79次
OC高级编程:内存管理、block、GCD

目录

一、block相关概念
 1、block的本质
 2、block的声明、实现、调用格式及block代码的执行顺序
二、block内存
 1、单个block的内存结构
 2、全局block、栈block、堆block
三、block捕获变量
 1、block捕获变量是什么意思?
 2、block捕获局部变量
 3、block捕获静态变量
 4、block不会捕获全局变量、静态全局变量
 5、block不会捕获成员变量,但是会捕获self
 6、用__block修饰局部变量
四、block的循环引用
 1、什么是block的循环引用
 2、使用__weak和__strong修饰符打破block的循环引用
五、block的应用场景:传值和回调

一、block相关概念


1、block的本质

首先需要知道基本的一点,block也是一种OC数据类型,只不过除了能像普通OC对象那样使用它外,它还具备了函数指针的功能。该数据类型的语法如下,好像有点奇怪:

returnType (^blockName)(parameters)
  • 一方面,block对象和普通OC对象是一样的:我们要始终记住block也是一种OC数据类型,所以我们就可以创建block对象,而且完全可以像使用普通OC对象那样使用block对象

  • 另一方面,block对象和普通OC对象又是有区别的:block毕竟是专门引入的一种特殊数据类型,所以block对象肯定有不同于普通OC对象的地方,那就是普通OC对象是用来包装和传递数据的,而block对象却和函数比较像,是用来包装和传递代码的,是一种为了替代函数指针而引入的语法结构

如果再多说一点,我们知道所有的OC对象都是一个结构体,所以block也不例外,它的本质也是一个结构体

// block的一部分成员变量
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

// block的一部分成员变量
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)};

// block的实现部分
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { 
    
    // 实现代码
    ......
}

// block结构体
struct __main_block_impl_0 {
    
  struct __block_impl impl;
  struct __main_block_desc_0 *Desc;
  
  // block的构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;//isa指针
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

结构体中有三个比较重要的成员变量(内存部分也会再提到)和一个构造函数:

  • isa:指向该block的类型。
  • 一个函数指针:指向该block的实现代码。
  • 还有一个结构体指针:声明了该block所占的内存大小,还有把block拷贝到堆区的copy函数的函数指针和把该block从堆区释放的dispose函数的函数指针等一些信息。
  • 此外,block结构体中还有一个block的构造函数用来完成block所有成员变量的初始化。
2、block的声明、实现、调用格式及block代码的执行顺序
  • (1)block的声明格式

block作为属性时,可以这样声明:

@property (nonatomic, copy) int (^block)(int a, int b);

block作为参数时,可以这样声明:

- (void)fecthDataWithCompletionHandler:(void (^)(NSData *data, NSError *error))completionHandler;

或者如果项目中大量使用相同类型的block,为了使block看起来更清晰,我们可以使用typedef重定义一下block的类型,再使用。如上面两例也可以写成下面这样:

typedef int (^Block)(int a, int b);

@property (nonatomic, copy) Block block;
typedef void (^Block)(NSData *data, NSError *error);

- (void)fecthDataWithCompletionHandler:(Block)completionHandler;
  • (2)block的实现格式:如果block没有返回值,可省略returnType;如果block没有参数,可省略parameters
returnType (^blockName)(parameters) = ^ returnType(parameters) {
    
    // 具体的实现代码
};
  • (3)block的调用格式:如果block没有参数,可省略parameters
blockName(parameters);
  • (4)block代码的执行顺序

block代码的执行顺序永远都是:block声明 --> block实现 --> block调用 --> 返回去寻找block的具体实现代码来执行。举例如下:

@interface ViewController ()

// 第一步:block的声明
@property (nonatomic, copy) int (^block)(int a, int b);

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    // 第二步:block的实现
    self.block = ^int(int a, int b) {
        
        // 第四步:返回来执行block的具体实现代码
        return a + b;
    };
    
    // 第三步:block的调用
    self.block(1, 1);
}

@end

二、block内存


1、单个block的内存结构
单个block的内存结构

上面第一部分中,我们提到block的本质就是一个结构体,那么这单个结构体所占内存的结构是怎样的呢?在单个block的内存结构中,有四个比较重要的变量:

  • 变量isa:指向该block的类型。
  • 函数指针invoke:指向该block的实现代码。
  • 结构体指针descriptor:声明了该block所占的内存大小,还有把block拷贝到堆区的copy函数的函数指针和把该block从堆区释放的dispose函数的函数指针等一些信息。
  • 捕获到的变量:由于block会捕获局部变量和静态变量,这个捕获过程就是把这些变量复制一份放到block自己的内存中,所以block的内存中会有专门的一块用来存放这些捕获到的变量。
2、全局block、栈block、堆block

block根据其存储域可以分为:全局block、栈block和堆block。那什么样的block是全局block、栈block和堆block呢?

  • 全局block

    • 在定义全局变量的地方就实现了的block是全局block。

    • 不在定义全局变量的地方实现了的block,但是block在实现的时候,其内部没有捕获局部变量或者静态变量,那么这样的block也是全局block。

#import "ViewController.h"

// 全局block
void (^block)(void) = ^ {
    
    NSLog(@"我是一个全局block");
};

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    block();

    // 全局block
    void (^block)()  = ^{
        
        NSLog(@"11.11");
    };
    block();
}

@end
  • 栈block:除了全局block外,剩下的就都是栈block了。

  • 堆block

    • 但是在实际操作中,如果直接用到了栈block,那么我们需要调用一下copy方法,手动的将栈block拷贝到堆区。

    • 如果我们把block作为属性使用,那么block的内存管理语义要用copy,让系统在适当的时机自动把栈block拷贝到堆区。

- (void)viewDidLoad {
    [super viewDidLoad];

    void (^block)(void) = [^ {
        
        NSLog(@"11");
    } copy];// 把栈block拷贝到堆区
}
// 用copy修饰block
@property (nonatomic, copy) void (^block)(void);

那为什么我们要把栈block拷贝到堆区呢?因为栈内存是由系统自动管理的,而栈block却只在声明和实现它的那部分代码范围内有效,所以一旦出了这个范围栈block就可能会被回收掉,这样我们在调用block的时候如果它已经被系统释放掉了,程序就会崩掉,所以我们需要把栈block拷贝到堆区。而这个拷贝过程是个深拷贝,它捕获的变量也会被copy到堆区(因为block捕获的变量本来就是block内存的一部分),这样就保证了block的生命周期,我们就可以更安全的使用block了。

三、block捕获变量


1、block捕获变量是什么意思?

我们所说的block捕获变量是指:当我们在编写block的实现代码时(注意block捕获变量的时机为编写block的实现代码时),如果block的内部使用了block外部的变量,那么系统就会对这些变量进行一次值拷贝,在block结构体内追加这些成员变量,并存储在block自身的内存中,作为block的一部分存在。切记这个拷贝过程为值拷贝(深拷贝),而不是指针拷贝(浅拷贝)。

注意:

  • 首先,并不是所有的block都会捕获变量,全局block不会捕获变量,栈block和堆block才会捕获变量,所以block捕获变量这句话的捕获方为栈block和堆block

  • 其次,栈block和堆block也不是会捕获所有的变量,它俩只会捕获局部变量和静态变量,而不会捕获全局变量、静态全局变量和成员变量,所以block捕获变量这句话的被捕获方为局部变量和静态变量,而block可以直接访问全局变量、静态全局变量和成员变量。

2、block捕获局部变量
  • 示例代码1:
- (void)viewDidLoad {
    [super viewDidLoad];

    NSString *string = @"11";// 第一句:定义一个局部变量
    void (^block)(void)  = [^{// 第二句:实现block,编写block内部的实现代码时,发现调用了外部的局部变量sting,于是捕获该string(即深拷贝一份到block内存中)
        
        NSLog(@"block内:%p,%@", &string, string);// 第七句:执行block的具体实现代码
    } copy];
    string = @"12";// 第三句:修改局部变量的值
    NSLog(@"block外:%p,%@", &string, string);// 第五句
    block();// 第六句:调用block,会去查找block的实现代码执行
}

打印:

block外:0x7ffeedd94aa8,12
block内:0x600000252b00,11
  • 示例代码2:
我们试图在block内部修改变量的值,发现不能修改

通过上面的两段示例代码,我们可以得出结论,block会捕获局部变量,但是会带来两个问题:

  • 问题1:修改block外局部变量的值,block内所捕获的该局部变量值不受影响。
    为什么会这样?因为block捕获变量的过程是个深拷贝,而不是浅拷贝,所以block外的变量和block内的变量已经是两个变量了。

  • 问题2:block内不能直接修改所捕获的局部变量的值。
    为什么会这样?这是系统决定的,我们也不知道人家为啥不让改,可能的考虑是:如果我们不去深究,光从表面看是根本不知道block内外的两个变量是截然不同的两个变量的,你在block内部修改变量值也改的是我们看不到的block内存中自己的那份,而不是大家可以看到的block外部的这份,所以系统索性就不让我们修改这个变量的值了。

但是如果我们真的想要修改block外局部变量的值,block内所捕获的该局部变量的值也跟着变,而且能在block内部修改局部变量的值呢?这两个问题真的没有解决办法吗?不是的,有两种办法可以解决这两个问题,分别是:

  • 解决办法1:直接改变局部变量的内存域,即把局部变量换成静态变量或全局变量或静态全局变量或成员变量。

  • 解决办法2:用__block修饰局部变量,间接改变局部变量的内存域,栈区到堆区。

3、block捕获静态变量

我们把上面的代码做下小修改,把局部变量换成静态变量

  • 针对上面问题1,修改如下:
- (void)viewDidLoad {
    [super viewDidLoad];

    static NSString *string = @"11";// 静态变量
    void (^block)(void)  = [^{
        
        NSLog(@"block内:%p,%@", &string, string);
    } copy];
    string = @"12";
    NSLog(@"block外:%p,%@", &string, string);
    block();
}

打印:

block外:0x10ccc24e8,12
block内:0x10ccc24e8,12
  • 针对上面问题2,修改如下:
- (void)viewDidLoad {
    [super viewDidLoad];

    static NSString *string = @"11";// 静态变量
    void (^block)(void)  = [^{
        
        string = @"33";
        NSLog(@"block内:%p,%@", &string, string);
    } copy];
    NSLog(@"block外:%p,%@", &string, string);
    block();
    NSLog(@"block外:%p,%@", &string, string);
}

打印:

block外:0x10d3a74e8,11
block内:0x10d3a74e8,33
block外:0x10d3a74e8,33

看打印输出,我们发现通过这种方式解决了上面的两个问题。那为什么能解决呢?

可以看下输出的指针,都是一样的,所以我们就可以知道block会捕获静态变量,但是捕获静态变量又不同于捕获局部变量,它是一个指针拷贝(即浅拷贝),所以block内外访问的局部变量其实都是静态全局区的同一个变量。

4、block不会捕获全局变量、静态全局变量
NSString *string = @"11";// 全局变量
static NSString *string = @"11";// 静态全局变量

我们把局部变量换成全局变量或静态全局变量,上面的两个问题同样能得到解决。

但是这两种方式之所以能解决的原因有和把局部变量改成静态变量不太一样,上面我们说到把局部变量改成静态变量能解决是因为block捕获静态变量是指针拷贝。而这两种方式能解决原因却是:block根本就不会捕获全局变量和静态全局变量,这些变量根本就没被复制到block自己的内存里作为block的一部分,所以block内外访问这些变量其实是直接访问全局静态区的变量的。

5、block不会捕获成员变量,但是会捕获self
@interface ViewController ()

@property (nonatomic, strong) NSString *string;

@end

我们把局部变量换成成员变量,上面的两个问题同样能得到解决。

但是这种方式和上面两种方式都不太一样,它捕获了东西,但也是直接访问当block内部使用成员变量的时候,block不会去捕获一个一个的成员变量,而只会捕获self变量,也就是说block结构体里会追加一个当前类型的self成员变量放在自己的内存里,而所有成员变量的访问都是通过self来访问的。成员变量在堆区,所以这也是一种直接访问。

6、用__block修饰局部变量

接下来,我们用__block修饰局部变量来试着解决一下。

  • 针对上面问题1,修改如下:
- (void)viewDidLoad {
    [super viewDidLoad];
    
    __block NSString *string = @"11";// 静态变量
    void (^block)(void)  = [^{
        
        NSLog(@"block内:%p,%@", &string, string);
    } copy];
    string = @"12";
    NSLog(@"block外:%p,%@", &string, string);
    block();
}

打印:

block外:0x60000025bc28,12
block内:0x60000025bc28,12
  • 针对上面问题2,修改如下:
- (void)viewDidLoad {
    [super viewDidLoad];

    __block NSString *string = @"11";// __block修饰局部变量
    void (^block)(void)  = [^{
        
        string = @"33";
        NSLog(@"block内:%p,%@", &string, string);
    } copy];
    NSLog(@"block外:%p,%@", &string, string);
    block();
    NSLog(@"block外:%p,%@", &string, string);
}

打印:

block外:0x60400064b398,11
block内:0x60400064b398,33
block外:0x60400064b398,33

可见,这种方式也确实可以解决,那为什么能解决呢?只要我们知道了__block修饰局部变量后发生了什么,这个问题也就回答了。

  • 首先,当我们用__block修饰局部变量后,这个局部变量会变成一个__block变量,所谓__block变量是指栈区会新生成一个结构体,原来的那个局部变量会作为一个成员变量放在这个结构体里,同时该结构体内部还生成了一个指向该结构体本身的__forwarding指针,此时__block变量依旧在位于栈区。而block内部则仅仅是新追加了一个指向这个__block变量的结构体指针,block就是通过这个结构体指针找到__block变量,再通过__forwarding指针找到原局部变量来访问的(注意:__block变量并不会被block捕获,它不是block本身的一部分,block仅仅是通过它新增的结构体指针来访问__block变量)。
__block变量 block
  • 然后,当我们把block从栈区拷贝到堆区的时候,这个__block变量也会从栈区被深拷贝一份到堆区,并被block所持有。与此同时,原来栈区__block变量的__forwarding指针不是指向它自己嘛,现在这个__forwarding指针会抛弃栈区原来的那个__block变量,转而也指向堆区拷贝出来的那一份__block变量。这样,无论我们在block内还是block外访问__block变量,其实都已经访问的是拷贝到堆区的那同一个__block变量了。

可见__block修饰局部变量的本质其实也是改变了局部变量的内存域,即把原来栈上的变量生成了一个__block变量给弄到了堆区。这个和block捕获静态变量的效果比较类似,静态变量是把局部变量弄到静态全局区并通过指针来访问,__block是把局部变量弄到堆区通过指针来访问,但两者之间还是有本质的区别的。

四、block的循环引用


1、什么是block的循环引用

当栈或者堆block在捕获变量后,如果该变量指向的是一个OC对象,并且该对象是strong语义的,那么当block从栈拷贝到堆区的时候,block就会自动持有该对象。所以如果恰好该对象也持有了block的话,就会造成循环引用。这样,OC对象和block就都无法正常释放,导致内存泄漏。

如下面的例子:self持有着作为成员变量的block,block也持有它捕获的self。

2、使用__weak和__strong修饰符打破block的循环引用

那怎么解决block的循环引用呢?我们把成员变量改为weak修饰可以吗?如下:

还是不行,因为造成循环引用的是self和block,这里和成员变量其实没什么关系,所以改了也没用。

那我们可以用__weak修饰符修饰self来解决block的循环引用问题

可见__weak修饰符能达到这样的效果。那__weak是怎么打破这个循环引用的呢?我们知道使用__weak修饰变量的作用就是使得被修饰的变量弱引用它所指向的对象,这里用__weak修饰了self之后,self就只会生成对block的弱引用,所以可以打破循环引用

但是通常我们又会在block内部刚开始执行创建一个对weakSelf的强引用,是为了避免block在执行之前self就被释放掉。这个强引用只是临时的,只是在block的作用域内有效,所以block一执行完的瞬间,强引用就会消失,恢复弱引用,所以是不会再次导致循环引用的。

所以打破block循环引用的正确方式应该同时使用__weak和__strong,这样比较稳妥,如下:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak id weakSelf = self;
    self.block = ^{
        
        __strong id strongSelf = weakSelf;
        
        NSLog(@"%@", strongSelf);
    };
}

使用__block虽然也能打破循环引用,但是需要一些额外的操作,所以推荐使用__weak和__strong。

五、block的应用场景:传值和回调


详见文章:代理设计模式,里面涉及到代理和block在应用于传值和回调时的对比和选型。

相关文章

  • iOS开发之Block原理探究

    Block概述 Block本质 Block调用 Block分类 Block循环引用 Block原理探究 Block...

  • block的使用

    定义block 返回类型 (^block名称)(参数) = ^(){block内容}; 调用block block...

  • Block 02 - __block

    Block 02 - __block __block 的作用 __block 可以解决 Block 内部无法修改 ...

  • iOS面试之Block大全

    Block Block内容如下: 关于Block 截获变量 __block修饰符 Block的内存管理 Block...

  • iOS面试之Block模块

    Block Block内容如下: 关于Block 截获变量 __block修饰符 Block的内存管理 Block...

  • iOS Block

    Block的分类 Block有三种类型:全局Block,堆区Block,栈区Block 全局Block 当Bloc...

  • iOS block 为什么官方文档建议用 copy 修饰

    一、block 的三种类型block 三种类型:全局 block,堆 block、栈 block。全局 block...

  • iOS开发block是用copy修饰还是strong

    Block分为全局Block、堆Block和栈Block1、在定义block没有引用外部变量的时候,block为全...

  • block 初探

    全局block, 栈block, 堆block

  • Block

    一、Block本质 二、 BlocK截获变量 三、__block 修饰变量 四、Block内存管理 五、Block...

网友评论

    本文标题:block

    本文链接:https://www.haomeiwen.com/subject/nevkbftx.html