iOS开发总结-Block(一)


前言

这篇文章主要是介绍在iOS开发过程中遇到的Block相关的问题。

作者使用的环境是:

  • Xcode 9正式版。
  • 真机调试 iPhone 6Plus 10.3.3
  • MacBook Pro 10.12.6

这篇文章主要会从以下几点进行讨论:

  • Block的结构
  • Block的类型
  • Block的实践
  • 总结

如果只想看总结请直接滑动到最底下看结论即可,中间的过程截图较多。


Block的结构

图片来源

iOS开发总结-Block(一)_第1张图片
image.png

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

  • isa 指针,所有对象都有该指针,用于实现对象相关的功能。
    这里的 isa 其实指向了一个类,每一个 block 指向的类可能是 _ NSGlobalBlock、_ NSMallocBlock 或者 _ NSStackBlock,但是这些 block,它们继承自一个共同的父类,也就是 NSBlock

  • flags,用于按 bit 位表示一些 block 的附加信息,下一篇介绍 block 的实现代码可 以看到对该变量的使用。

  • reserved,保留变量。

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

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

  • variables,capture 过来的变量。
    block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。


Block的类型

上面也提到一些,Block的类型在iOS上有三种:

  • _ NSGlobalBlock
    全局的静态block,不会访问任何外部变量;
  • _ NSStackBlock
    位于栈内存,出栈时会被销毁,函数返回后Block将无效(非ARC下,即使retain也不行,例如将block加到一个数组中,正确的做法是copy到堆中再返回copy得到的block);
  • _ NSMallocBlock
    位于堆内存,当引用计数为0时会被销毁。

这个地方可以多想一下,为什么block要分这么多种类。
Stack Overflow上有个回答我觉得挺合适的:

Global and Stack refer to where the captured data resides at the time the block is defined. If a block is Global the runtime knows that no further processing will need to be done. Things like copy become a no-op. If the block is Stack, then the runtime is aware that the data needs to be moved if it will go out of scope before the block is released. This is especially important for ARC.


下面针对block的类型进行具体分析
ARC

创建的代码如下:

    TestBlockExample block1 = ^{
        printf("Hello, World!\n");
    };
iOS开发总结-Block(一)_第2张图片
image.png

可以从图上看到在ARC下创建的一个没有捕获任何变量的的block是 _ NSGlobalBlock 类型的,下面将代码更改一下:

    int a = 6;
    NSLog(@"block类型:%@",^{
        NSLog(@"Hello, World! %d\n", a);
    });
iOS开发总结-Block(一)_第3张图片
image.png

捕获了变量但是没有任何强引用是 _ NSStackBlock 类型。我们再看看正常的使用,block被强引用的情况:

    int a = 6;
    TestBlockExample block1 = ^{
        NSLog(@"Hello, World! %d\n", a);
    };
iOS开发总结-Block(一)_第4张图片
image.png

此时由于block捕获了a变量,并且被block1强引用,所以block类型由 _ NSStackBlock 变成了 _ NSMallocBlock 类型。
如果 TestBlockExample block1 前面加上 __weak 修饰,此时 block类型就成了 _ NSStackBlock ;

在ARC下,以下几种情况, Block会自动被从栈复制到堆(变成 _ NSMallocBlock 类型):

1.执行copy方法得到的新的block(原始的block类型不会改变)
2.作为方法返回值(本质就是第一种情况)
3.创建时将Block赋值给附有__strong修饰符的id类型的类或者Blcok类型成员变量时
4.在方法名中含有usingBlock的Cocoa框架方法或者GCD的API中传递的时候.(也是第一种情况)

下面看一下 MRC的情况

MRC
MRC 没有捕获变量

创建的代码如下:

    TestBlockExample block1 = ^{
        NSLog(@"Hello, World! \n");
    };

运行结果

iOS开发总结-Block(一)_第5张图片
image.png

下面将代码更改一下,我们使用copy看看效果:

iOS开发总结-Block(一)_第6张图片
image.png

在非ARC下创建的一个没有捕获任何变量的的block是 _ NSGlobalBlock 类型的,并且调用copy以后新的block2 的类型和代码块地址都不会发生改变(本质上就是retainCount+1),下面将代码更改一下,我们看看有变量捕获的情况:

MRC 捕获变量
    NSNumber *a = @6;
    TestBlockExample block1 = ^{
        NSLog(@"Hello, World! %@\n", a);
    };
iOS开发总结-Block(一)_第7张图片
image.png

捕获了变量block是 _ NSStackBlock 类型的。

    NSNumber *a = @6;
    TestBlockExample block1 = ^{
        NSLog(@"Hello, World! %@\n", a);
    };
    NSLog(@"block1:%p", &block1);
    TestBlockExample block2 = [block1 copy];
    NSLog(@"block1:%p, block2:%p", &block1, &block2);
iOS开发总结-Block(一)_第8张图片
image.png

注意看图中标记的部分
调用copy时,block1和block2的代码块地址是一致的,但是block2的类型由 _ NSStackBlock 变成了 _ NSMallocBlock ,
其实这一步操作本质上是将block1由栈中copy到了堆中,但是代码块地址始终是同一个。此时block1和block2的内存地址并不一样。注意不要将代码块地址和block变量地址搞混淆了。

总结

  • 只要是未捕获变量都是__NSGlobalBlock类型;
  • 捕获了变量的 block 一开始创建的时候就是在栈中,对应的就是__NSStackBlock类型,使用如下一些操作会将block移到堆中(这个地方我觉得是对block本身做了一个副本,至于引用的代码块则始终只有一份)。
在ARC下,以下几种情况, Block会自动被从栈移到堆中(变成 _ NSMallocBlock 类型):
1.执行copy方法得到的新的block(原始的block类型不会改变)
2.作为方法返回值(本质就是第一种情况)
3.创建时将Block赋值给附有__strong修饰符的id类型的类或者Blcok类型成员变量时
4.在方法名中含有usingBlock的Cocoa框架方法或者GCD的API中传递的时候.(也是第一种情况)
MRC下,需要手动调用copy 才可以将Block从栈移到堆中

这里面所有的移到堆中的意思其实就是在堆中做了block的副本,对于原来在栈中的block,类型不会改变。

另外需要注意的是block的类型和__block修饰符没有关系。
现在得到的结论,仅在相同的调试环境下可以证明,其他的环境我并没有尝试。
关于__block的使用这里就不在赘述了,大致结论就是:

ARC下 __block 修饰的变量 block 是地址拷贝,不加 __block 是值拷贝;
MRC下 __block主要用来消除循环引用的问题。

下一篇会介绍具体的__block的作用。


参考的相关资料

对Objective-C中Block的追探
谈Objective-C block的实现
A look inside blocks: Episode 3 (Block_copy)
你真的理解__block修饰符的原理么?

你可能感兴趣的:(iOS开发总结-Block(一))