在iOS之Block基本使用中,我们介绍了一些有关Block的基本知识,以及基本用法,在这里,我们将继续围绕Block来讲解,学习有关Block的本质问题。

首先,在学习之前,增加一些动力。经常在面试中,会被问及到这些问题:

block的本质是什么?

__block的作用是什么?原理是什么?有哪些使用注意点?

我们知道block在使用的时候,一般用copy修饰,用copy修饰发生了什么?具体过程是怎样的?

带着这些疑问,我们开始今天的学习。


block的数据结构长什么样?

首先,我们写一个简单的block,以及block的调用:

int age = 10;
void(^block)(int, int) = ^(int a, int b){
    NSLog(@"调用该block----%d", age);
};
block(100, 100);

通过clang编译指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m将main.m文件转换为底层代码,通过查找可以看到上面代码转换为底层代码的相关代码:

int age = 10;
void(*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 100, 100);

可以看到,block最后被转换为__main_block_impl_0类型

__main_block_impl_0类型是个什么样的结构存在的呢?

通过查看定义能够知道,__main_block_impl_0的定义为:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;//函数调用的外部参数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

其中,__main_block_impl_0里面第一个类型__block_impl和第二个类型__main_block_desc_0的定义分别为:

struct __block_impl {
  void *isa;//isa指针
  int Flags;
  int Reserved;
  void *FuncPtr;//函数地址
};

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
}

首先,我们看到__main_block_func_0是一个函数;

main函数中__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)将参数__main_block_func_0传到了block里面,赋值给__main_block_impl_0里面的fp,然后做了 impl.FuncPtr = fp。

最后在执行block的时候,是执行的block->FuncPtr,就调用到了impl.FuncPtr,也就是fp,也就是__main_block_func_0

block封装 ios ios block的原理_封装


block内部直观表示大致如下:

block封装 ios ios block的原理_全局变量_02

总结:

1. block是一个具有isa指针的oc对象
2. block是封装了函数以及函数调用环境的OC对象

封装的函数是指block{}内部的代码,被转换成一个函数__main_block_func_0,并将函数地址封装在了__main_block_impl_0(block类型)内部的impl.FuncPtr
函数调用环境是指,函数调用的时候需要的参数,从图中可以看出,函数需要的变量age,已经被封装在了__main_block_impl_0里面。


接下来,我们分析下block转换为底层源码的代码

int age = 10;
void(*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age)); 上句代码是Block的定义
一般小括号为强制转换,为了方便观察,可以将小括号以及小括号里面的内容删掉。
简化为:
void(*block)(int, int) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age)); 等号后面,是一个函数,函数名为__main_block_impl_0,函数有三个参数。并且获取函数地址后赋值给block对象。也就是block是一个指针变量。其内部存放的是__main_block_impl_0类型的地址。

而查看__main_block_impl_0的定义

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;//函数调用的外部参数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;//block的类型是_NSConcreteStackBlock
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

__main_block_impl_0函数与结构体名一样,该函数没有写返回值,但其实是返回结构体本身,该函数称为构造函数。
那么,block其实指向的是__main_block_impl_0结构体的地址。

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)};

sizeof(struct __main_block_impl_0):__main_block_impl_0即block的大小

((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 100, 100); 上句代码为block的执行,简化后的结果为:
(block->FuncPtr)(block, 100, 100); 利用block找到FuncPtr函数,进行调用。

看一下(block->FuncPtr)(block, 100, 100); block即__main_block_impl_0类型,里面并没有FuncPtr函数
__main_block_impl_0里面的__block_impl类型里面才有FuncPtr函数,怎么block直接就调用了FuncPtr函数呢?

这是因为,里面有个强制转换操作,将block强制转换为__block_impl *类型,这样,就可以直接访问__block_impl里面的FuncPtr函数,即__block_impl->FuncPtr
那,为什么这个可以将__main_block_impl_0强制转换为__block_impl类型呢?
这是因为,结构体__block_impl类型是__main_block_impl_0类型的第一个成员,那么__main_block_impl_0类型的内存地址跟__block_impl类型的内存地址是一样的,因此,可以强制转换。

从另一个角度去分析,__main_block_impl_0里面的__block_impl是一个结构体,而不是指针,相当于直接把__block_impl类型的内容放入__main_block_impl_0之中,也就相当于可以直接进行__main_block_impl_0->FuncPtr访问。


block捕获机制

block内部访问局部变量

来段简单的代码:

int age = 10;
void(^block)(void) = ^{
    NSLog(@"调用该block----%d", age);
};
age = 20;
block();

很容易,我们知道最后的运行结果是:

调用该block----10

那么,是怎样一个原理呢?

同样,我们通过clang命令,将代码转换为底层代码:

int age = 10;

void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

age = 20;

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

可以看到,block将age作为参数,传到__main_block_impl_0里面。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

在__main_block_impl_0里面,block自己定义了一个同名变量age。
并通过age(_age)将_age的值赋值给age,即
age(_age) 等价于 age = _age;

执行
age = 20;
只是将int age = 10变为int age = 20,并没有改变block里面age的值

执行
block();
调用block实现,就调用了__main_block_impl_0里面的FuncPtr函数,而FuncPtr函数里面已经封装了age

static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
{
  int age = __cself->age; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_91_sht6gqgs1xj8b0_gczwc0ygc0000gn_T_main_65a3fe_mi_0, age);
}

该age是__cself->age,即block内部的age。而block内部的age=10。
因此,打印出来的age=10;也就是常说的值传递。

为什么值传递的值不可以赋值或者修改呢?

这个我们留到下一小节进行讲述


block内部访问static修饰的局部变量

如果用static修饰,会是怎么样呢?

int age = 10;
static int height = 170;
void(^block)(void) = ^{
    NSLog(@"调用该block----%d, %d", age, height);
};
age = 20;
height = 180;
block();

结果:调用该block----10, 180
int age = 10;
static int height = 170;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
age = 20;
height = 180;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

从上面可以看出,age传的是值,height是传的指针

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;//新建同名变量age
    int *height;//新建同名变量height,但是此height是指针变量
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

__main_block_impl_0内部,新建的age是int,而height是指针int *。

执行height = 180;

执行block();

static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
{
  int age = __cself->age; // bound by copy
  int *height = __cself->height; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_91_sht6gqgs1xj8b0_gczwc0ygc0000gn_T_main_48f888_mi_0, age, (*height));
}

调用的时候,age是值传递,height是指针

由于height传的值是指针,*height已经修改为180,因此,block内部的height也被修改,因此最后打印出来的height是180

block封装 ios ios block的原理_block封装 ios_03

block内部访问全局变量

如果是全局变量或者static修饰的全局变量,运行结果又有什么不一样呢?
int age = 10;
static int height = 170;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^block)(void) = ^{
            NSLog(@"调用该block----%d, %d", age, height);
        };
        age = 20;
        height = 190;
        block();
    }
    return 0;
}

运行结果:调用该block----20, 190

转换为底层代码后:

block封装 ios ios block的原理_封装_04

从底层源码可以看出,__main_block_impl_0内部没有新定义age,或者height。

说明block内部并没有捕获外部的全局变量。

最后调用函数,打印的age和height是全局变量。


通过以上代码,我们可以发现,block访问外部变量,有一个变量捕获机制(capture)

捕获机制

怎么理解捕获呢?

在block内部专门新建一个变量,用来存储外部的值,称为捕获。
通过以上例子,可以得出:

block访问外部变量总结:

block封装 ios ios block的原理_ios_05

为什么使用auto修饰的变量,block捕获的值,而使用static修饰的局部变量,block捕获的是指针呢?

这是因为,auto修饰的变量,随时可能被销毁,因此,需要及时把值捕获进去。
而static修饰的变量,在程序整个生命周期都存在,所以,可以对变量进行修改,因此只需要捕获指针即可。
全局变量,存储在静态全局区,整个程序的生命周期都存在,因此,不需要捕获
参考:iOS 静态、全局变量、常量

- (void)test
{
    void(^block)(void) = ^{
        NSLog(@"调用该block----%@", self);
    };
    block();
}

问:该block里面的self,是否会被捕获?

同样,使用clang转换为底层代码,可以看到block定义:

struct __YZPerson__test_block_impl_0 {
  struct __block_impl impl;
  struct __YZPerson__test_block_desc_0* Desc;
  YZPerson *self;
  __YZPerson__test_block_impl_0(void *fp, struct __YZPerson__test_block_desc_0 *desc, YZPerson *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到,捕获到了self。

问:为什么会捕获self呢?
- (void)test
{
    void(^block)(void) = ^{
        NSLog(@"调用该block----%@", self);
    };
    block();
}

最后转化为:

static void _I_YZPerson_test(YZPerson * self, SEL _cmd) {
    void(*block)(void) = ((void (*)())&__YZPerson__test_block_impl_0((void *)__YZPerson__test_block_func_0, &__YZPerson__test_block_desc_0_DATA, self, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

可以看出,在test函数里面,其实是有两个隐式变量:self和SEL类型的_cmd
self是以参数传进去的,因此,self属于局部变量,需要进行捕获。

既然这样的话,那么:

- (void)test
{
    void(^block)(void) = ^{
        NSLog(@"调用该block----%@", _name);
    };
    block();
}
_name又是如何存在的呢?捕获还是不捕获?捕获的话是直接捕获还是怎样的呢?

不多说,咱还是直接看底层代码:

block封装 ios ios block的原理_全局变量_06


从图片中可以看到,block并没有捕获name,而是通过捕获的self,访问的_name。

其实可以理解,因为name属于YZPerson里面的一个属性,_name是YZPerson里面的一个成员变量,_name其实是self->_name,因此,是通过捕获self,访问_name成员变量的。

这样说明了,在block内部通过访问成员变量,就相当于里面引用了self,因此,还是需要留意循环引用的问题。


block类型

block有三种类型:

__NSGlobalBlock__
__NSStackBlock__
__NSMallocBlock__

block的三种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承NSBlock类型,再往上是继承NSObject类型。

举个例子:

void(^block)(void) = ^{
    NSLog(@"调用该block----");
};

NSLog(@"1-%@", [block class]);
NSLog(@"2-%@", [[block class] superclass]);
NSLog(@"3-%@", [[[block class] superclass] superclass]);
NSLog(@"4-%@", [[[[block class] superclass] superclass] superclass]);
NSLog(@"5-%@", [[[[[block class] superclass] superclass] superclass] superclass]);
NSLog(@"6-%@", [[[[[[block class] superclass] superclass] superclass] superclass] superclass]);

运行结果:
2020-03-25 11:08:14.326871+0800 block学习[53532:3297757] 1-__NSGlobalBlock__
2020-03-25 11:08:14.327226+0800 block学习[53532:3297757] 2-__NSGlobalBlock
2020-03-25 11:08:14.327276+0800 block学习[53532:3297757] 3-NSBlock
2020-03-25 11:08:14.327317+0800 block学习[53532:3297757] 4-NSObject
2020-03-25 11:08:14.327349+0800 block学习[53532:3297757] 5-(null)
2020-03-25 11:08:14.327377+0800 block学习[53532:3297757] 6-(null)

可以看出,该block的类型是 NSGlobalBlock,其继承关系是:

__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject

至于为什么NSObject的superclass是nil可以参考iOS中对象的本质。

问:那,三种类型具体什么时候是哪种类型呢?

先上个总结图:

block封装 ios ios block的原理_全局变量_07

具体的实验结果可以参考iOS之Block基本使用

其中,在ARC下,你会发现,

int a = 3;//局部变量
void(^block)(void) = ^{
    NSLog(@"调用了block, a = %d", a);
};
NSLog(@"%@", block);
结果:<__NSMallocBlock__: 0x28343ea60>

按照之前的总结图,block的存储类型不应该是NSStackBlock吗?怎么打印出来的却是NSMallocBlock?

这是因为,在ARC中,系统以及自动帮我们做了copy操作。从而将本应该是NSStackBlock经过copy操作后,变为NSMallocBlock。

每一种类型的block调用copy后的结果如下:

block封装 ios ios block的原理_强制转换_08

为什么ARC需要帮我们把本存储在NSStackBlock的block经过copy操作,转移存储在NSMallocBlock上呢?

一个在MRC下的例子:

void(^block)(void);
void test()
{
    int age = 10;
    block = ^{
       NSLog(@"调用该block----%d", age);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
    return 0;
}

结果:
调用该block-----272632728

可以看到,age的值不是10,而是一串莫名其妙的数字

这是因为,block引用了auto变量,block类型是NSStackBlock。
在test()括号执行完毕后,block其实已经被释放了,再次调用block,里面的age就不是10了。
因此,我们需要将存在栈上的block通过copy操作,转移存储在堆上。将生命周期交给程序员自己控制。

总结:

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

  • block作为函数返回值时
  • 将block赋值给strong指针时(即block有强指针引用)
  • block作为Cocoa API中方法名含有usingBlock的方法参数时
  • block作为GCD API的方法参数时

三个block类型具体存储在哪一个区域

block封装 ios ios block的原理_强制转换_09


block与copy、retain、release操作

对不同类型的block,调用其retainCount,观看其有何不同点:

以下是验证程序:

NSGlobalBlock

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //没有访问auto变量,存储在NSGlobalBlock
    void(^block)(void) = ^{
        
    };
    NSLog(@"%@", block);
    [block retain];
    [block retain];
    [block retain];
    NSLog(@"[block retainCount] = %d", [block retainCount]);
    NSLog(@"%@", block);
}

打印结果:
2020-07-15 18:42:26.133369+0800 block2[9804:1002328] <__NSGlobalBlock__: 0x10fd29178>
2020-07-15 18:42:26.133506+0800 block2[9804:1002328] [block retainCount] = 1
2020-07-15 18:42:26.133586+0800 block2[9804:1002328] <__NSGlobalBlock__: 0x10fd29178>

NSStackBlock

- (void)viewDidLoad {
    [super viewDidLoad];
    
    int a = 3;//局部变量
    //访问auto变量,存储在NSStackBlock
    void(^block)(void) = ^{
        NSLog(@"调用了block, a = %d", a);
    };
    NSLog(@"%@", block);
    [block retain];
    [block retain];
    [block retain];
    NSLog(@"[block retainCount] = %d", [block retainCount]);
    NSLog(@"%@", block);
}

打印结果:
2020-07-15 18:43:43.936774+0800 block2[9825:1003381] <__NSStackBlock__: 0x7ffeebdf7f88>
2020-07-15 18:43:43.936910+0800 block2[9825:1003381] [block retainCount] = 1
2020-07-15 18:43:43.936996+0800 block2[9825:1003381] <__NSStackBlock__: 0x7ffeebdf7f88>

NSMallocBlock

- (void)viewDidLoad {
    [super viewDidLoad];
    
    int a = 3;//局部变量
    //访问auto变量,存储在NSStackBlock
    void(^block)(void) = [^{
        NSLog(@"调用了block, a = %d", a);
    } copy];//调用copy,存储在NSMallocBlock
    NSLog(@"%@", block);
    [block retain];
    [block retain];
    [block retain];
    NSLog(@"[block retainCount] = %d", [block retainCount]);
    NSLog(@"%@", block);
}

打印结果:
2020-07-15 18:44:48.964249+0800 block2[9842:1004304] <__NSMallocBlock__: 0x600000cd53e0>
2020-07-15 18:44:48.964366+0800 block2[9842:1004304] [block retainCount] = 1
2020-07-15 18:44:48.964456+0800 block2[9842:1004304] <__NSMallocBlock__: 0x600000cd53e0>

block与copy、retain、release操作的总结:

不同于NSObjec的copy、retain、release操作:

Block_copy与copy等效,Block_release与release等效;

对Block不管是retain、copy、release都不会改变引用计数retainCount,retainCount 始终是1

NSGlobalBlock:retain、release、copy操作都无效;

NSStackBlock:retain、release操作无效,必须注意的是,NSStackBlock在函数返回后,Block内存将被回收。即使retain也没用。容易犯的错误是[[mutableAarry addObject:stackBlock],在函数出栈后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock copy到堆上,然后加入数组:[mutableAarry addObject:[[stackBlock copy] autorelease]]。
支持copy,copy之后生成新的NSMallocBlock类型对象。

NSMallocBlock:支持retain、release,虽然retainCount始终是1,但内存管理器中仍然会增加、减少计数。
copy之后不会生成新的对象,只是增加了一次引用,类似retain;

尽量不要对Block使用retain操作。

更多学习请参考正确使用Block避免Cycle Retain和Crash