一、Block的基础知识
Block是对象,可以赋值,当参数,也可以放入Array和Dictionary中,可以retain和release。Block用“^”符号来表示,后面跟一对花括号,括号里是Block的实现代码。例如,下面就是一个简单的Block:
^{
// Block implementation here
}
Block其实就是一个值,而且有其相关类型。与int、float或Objective-C对象一样,也可以把块复制给变量,然后像使用其他变量那样使用它。块类型的语法与函数指针近似。下面列出的这个块很简单,没有参数,也不返回值:
void (^someBlock)() = ^{
// Block implementation here
}
这段代码定义了一个名为someBlock的变量。由于变量名写在正中间,所以看上去有点怪。然而Block会在不同的场景下有各种不同的写法。
Block难记的语法:
// 作为变量:
returnType (^blockName)(parameterTypes) = ^returnType(parameter) {…};
// 作为属性:
@property (nonatomic, copy) returnType (^blockName)(parameterTypes);
// 作为函数声明中的参数:
- (void)someMethodBlock:(returnType (^)(parameterTypes))blockName;
// 作为函数调用中的参数:
[obj someMethodBlock:^returnType (parameters) {...}];
// 作为typedef:
typedef returnType (^TypeName)(parameterTypes); // 为block类型重命名
TypeName blockName = ^returnType(parameters) {…}
// 例如:
// Block字面值:
double ^(double first, double second) {
return first + second;
}
// 声明并使用:
double (^total)(double, double) = ^double (double first, double second) {
return first + second;
};
double result = total(2, 4);
二、Block的类型
块的强大之处:在声明它的范围里,所有变量都可以为其所捕获。也就是说,那个范围里的全部变量,在块里依然可以用。下面来举例说明一下各种类型的块:
1、全局块(Global Block)
当Block没有访问auto变量(自动变量,离开了其作用域就会被销毁的),即:访问了全局变量 or 访问了静态局部变量 or 没有访问变量,此时的Block属于全局块NSConcreteGlobalBlock
。块所使用的整个内存区域,在编译期已经完全确定了,因此全局块可以声明在全局内存里,而不需要在每次用到的时候于栈中创建(跟其他类型的block相比)。对全局块执行copy是一个空操作,因为全局块绝不可能为系统所回收。这种块实际上相当于单例。下面就是一个全局块:
// NSGlobalBlock:存储于全局数据区,由系统管理
// 没有访问auto变量(也叫自动变量,离开作用域就销毁的变量)
// 如:没有访问任何变量 或 访问了全局变量 或 访问了静态局部变量
static int localCount = 0; // 静态局部变量
void(^blockA)(void) = ^{
NSLog(@"blockA"); // 没有访问任何变量
NSLog(@"%@", [MOManager sharedInstance].name); // 访问了全局变量
NSLog(@"%d", localCount); // 访问了静态局部变量
};
NSLog(@"blockA: %@", [blockA class]);
由于运行该块所需的全部信息在编译期确定,所以可把它做成全局块。这完全是一种优化技术:若把如此简单的块当成复杂的块来处理,那就会在复制及销毁该块时执行一些无谓的操作。
2、栈块(Stack Block)
当Block访问了栈区的变量(如:局部变量),此时的Block存储在栈区。其所占用的内存区域是分配在栈中的。也就是说,Block只在定义它的那个范围内有效。下面就是一个栈块:
// NSStackBlock 栈
// 访问了auto变量(即:栈区变量/局部变量), 并且 没有被`强指针`引用时!!!
int count = 0; // 局部变量
MOPerson *obj = [[MOPerson alloc] init]; // 局部变量
NSLog(@"blockB: %@", [^{
NSLog(@"%d", count); // 访问了局部变量
NSLog(@"%@", obj.name); // 访问了局部变量
NSLog(@"%@", self.property); // 访问了局部变量
} class]);
3、堆块(Malloc Block)
当Block访问了堆区的变量(如:alloc创建的对象)或 “栈块”调用了copy,此时的Block属于“堆块”。其所占用的内存区域是分配在堆中的。拷贝到堆上Block就成了带引用计数的对象了。后续再执行copy都不会真的执行复制,只是递增Block对象的引用计数。下面就是一个堆块:
// NSMallocBlock 堆
// 访问了auto变量,并且 被`强指针`引用时!!!
void(^blockC)(void) = ^{
NSLog(@"%d", count); // 访问了局部变量
NSLog(@"%@", obj.name); // 访问了局部变量
NSLog(@"%@", self.property); // 访问了局部变量
};
NSLog(@"blockC: %@", [blockC class]);
将Block的类型列表如下:
Type | 实现 | copy |
---|---|---|
Global(全局块) | 没有访问auto变量(离开作用域就销毁的变量),即:访问全局变量/静态局部变量/没有访问变量 | 什么也不做 |
Stack(栈块) | 访问了auto变量(即:栈区变量/局部变量), 并且 没有被强指针 引用时!!! | 复制到堆区 |
Malloc(堆块) | stack Block 调用copy函数时;访问了auto变量,并且 被强指针 引用时!!!;ARC模式下,以下4种情况被自动copy | 引用计数+1 |
4、ARC自动copy的块
- Block作为函数返回值
- Block赋值给
__strong
指针时(即被强指正引用时,如:__strong
修饰的id类型/Block类型的成员变量) - Block作为
Cocoa API
中的方法含有usingBlock
的方法参数时 - Block作为
GCD API
的方法参数时
三、Block捕获变量+底层实现
先看一下概览:
static int localCount = 0; // 静态局部变量
int count = 0; // 局部变量
MOPerson *obj = [[MOPerson alloc] init];
NSMutableArray *arr = [NSMutableArray array];
__block int blockCount = count;
__block MOPerson *blockObj = obj;
__block typeof(arr) blockArr = arr;
void (^aBlock)(void) = ^{
// 直接访问
NSLog(@"%d", globalCount); // 访问全局变量:直接使用,结构不变
NSLog(@"%d", localCount); // 访问静态局部变量:拷贝指针 到block的结构体 中使用
NSLog(@"%d", count); // 访问局部基本数据类型:拷贝值 到block的结构体 中使用
NSLog(@"%@", obj.name); // 访问alloc对象:拷贝指针 到block的结构体 中使用
NSLog(@"%@", arr); // 访问alloc对象:拷贝指针 到block的结构体 中使用
// 可直接修改的变量:
globalCount = 1; // 全局变量
localCount = 2; // 静态局部变量
obj.name = @"momo"; // 局部变量的属性 (仅修改,而不是重指向)
[arr addObject:@1]; // 局部可变collection(仅修改,而不是重指向)
// 需要__block修饰,才能修改的
// 会创建__Block_byref_XX_0结构体包装该变量,将此对象的指针拷贝到block结构体中使用
blockCount = 3; // 局部基本数据类型
blockObj = [[MOPerson alloc] init]; // 局部alloc变量
blockArr = [NSMutableArray array];
};
aBlock();
下面一一看其结构:
1、未捕获变量
我们先来看一下未捕获变量时Block的底层实现和内部结构。先在main函数里写一个简单的Block:
void(^aBlock)(void) = ^{
NSLog(@"Hello world");
}
aBlock();
command+S保存一下,然后打开“终端”(terminal):cd + (拖入mian.m所在文件夹),回车。然后输入:(注:后面main.m
是文件名,你如果写在其他文件里,就应该改成其他文件的名字)
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
回车后就会生成main.m文件的.cpp文件,即转换成编译后的C/C++文件,打开查看实现代码。实现代码很多,不过不要方,我们需要看的代码就几行,在最下面:
如上图一共5个部分,分别是:block的信息、block的结构体、block的实现、block的描述、main函数。
- block的信息:包含4个信息,其中FuncPtr为block括号里的实现方式
- block的结构体:由两个结构体组成,分别是block的实现和block的描述。(红色线条标注的)
- block的实现:即我们写在block中括号里的代码
- block的描述:仅有两个值,reserved表示保留变量占用的内存大小,Block_size表示Block结构体(__main_block_impl_0)占用的内存大小
- main函数:定义了block,并通过block的结构体
__main_block_impl_0
进行初始化,传入了两个参数:第一个参数是block括号里的实现(蓝色线条标注的),第二个参数是block的描述(绿色线条标注的)。最后是block的调用,一目了然。
画了结构图如下:
(因为没有访问任何变量,所以此block是global类型的)
注:另外捕获全局变量时,block的结构跟没有捕获时一样。全局变量直接在实现方法里使用,不需要捕获。
2、捕获基本数据类型
写一个使用基本数据类型
的block如下:
int a = 1;
void(^aBlock)(void) = ^{
NSLog(@"%d", a);
}
aBlock();
继续使用terminal
转换为底层实现代码如下:
我们现在只看跟上一个(不捕获变量时)的区别:首先block的结构体变了,多了需要捕获的变量,其初始化方法也多个一个参数,用来传入需要捕获的值。
其结构图如下:
3、捕获alloc变量
写一个使用alloc变量
的block如下:
NSObject *obj = [[NSObject alloc] init];
void(^aBlock)(void) = ^{
NSLog(@"%@", obj);
}
aBlock();
继续使用terminal
转换为底层实现代码如下:
其结构图如下:
可以看到当使用了alloc对象
时,跟之前的对比:block的描述(Desc)会多出两个方法:copy和dispose。
- copy:其内部调用了
__Block_object_assign
实现对变量的引用方式,会根据变量的类型
实现不同的引用方式,在block初始化的时候调用 - dispose:其内部调用了
__Block_object_dispose
实现对变量的释放,在block销毁的时候调用
assgin和dispose:最后一个参数表示变量的引用方式,大致有以下几种,分别用来描述不同类型的变量:
enum {
BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ...
BLOCK_FIELD_IS_BLOCK = 7, // a block variable
BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable
BLOCK_FIELD_IS_WEAK = 16, // declared __weak
BLOCK_BYREF_CALLER = 128, // called from byref copy/dispose helpers
};
_Block_object_assign
函数会根据所指向对象的修饰符(__strong
、__weak
、__block
、__unsafe_unretained
)做出相应的操作,形成强引用(retain)或者弱引用。
4、捕获静态局部变量
写一个使用静态局部变量
的block如下:
static NSString *name = @"mo";
void(^aBlock)(void) = ^{
NSLog(@"%@", name);
}
aBlock();
继续使用terminal
转换为底层实现代码如下:
结构如下图:
跟上一个捕获alloc变量对比,捕获静态变量使用的是 指向指针的指针,所以可以直接使用或修改静态变量的值。(捕获局部静态变量的block也是global类型的,因为局部静态变量也是存放在全局区的)
5、__block
捕获变量
默认情况下,为块所捕获的变量,是不可以在块里修改的(除了全局变量和静态局部变量)。像上述的2(捕获基本数据)和3(捕获alloc对象)捕获的变量是无法在block内部修改的(只能读值),若想要在block内部修改这样的变量,需要用__block
进行修饰后使用,例如:
__block int a = 1;
void(^aBlock)(void) = ^{
NSLog(@"%d", ++a);
}
aBlock();
继续使用terminal
转换为底层实现代码如下:
结构如下图:
跟之前的对比,可以看到当用__block
修饰变量时,系统会为这个变量创建一个结构体__Block_byref_(变量名)_0
,包装需要捕获的变量。结构体包含的主要内容:
__forwarding
:是指向__Block_byref_(变量名)_0
结构体的指针,结合block结构体的初始化方法可以看出,将__Block_byref_(变量名)_0
结构体a的__forwarding
赋值给了block结构体属性a。- a(变量名):真正使用到的变量,使用的时候用:
a->__forwarding->a
当经过了block的初始化后,__Block_byref_a_0
结构体被拷贝到堆上,具体底层怎么拷贝怎么赋值的这里不深究:
-
为啥不直接使用a->a,而是
a->__forwarding->a
呢?这是因为,如果变量在栈上,就可以直接访问,但是如果已经拷贝到堆上,访问的时候还去栈上访问就会出现问题,所以根据
__forwarding
找到堆上的地址,然后再使用:
__block拷贝的是 指针对象的内存地址(修饰之前拷贝的是指针)
想改的是:指针的重指向
而不是:指向对象的内容
所以以下几种情况是可以的:
NSMutableString *str = [NSMutableString stringWithString:@"mo"];
NSMutableArray *arr = [NSMutableArray arrayWithArray:@[@1, @2]];
void(^aBlock)(void) = ^{
str.string = @"moxiaoyan"; // 修改内容(可以)
// str = [NSMutableString stringWithString:@"moxiaoyan"]; // 重定向(报错)
[arr addObject:@3]; // 修改内容(可以)
// arr = [NSMutableArray arrayWithArray:@[@1, @2, @3]]; // 重定向(报错)
// ... 等等
}
- 为什么Block不能直接修改外部变量的值呢?
Apple这样设计,应该是考虑到了block的特殊性,block 本质上是一个对象,block 的花括号区域是对象内部的一个函数,变量进入 花括号,实际就是已经进入了另一个函数区域—改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。
所以 Apple 在编译器层面做了限制,如果在 block 内部试图修改 auto 变量(无修饰符),那么直接编译报错。
你可以把编译器的这种行为理解为:对 block 内部捕获到的 auto 变量设置为只读属性—不允许直接修改。
注:因而__block
不能修饰全局变量和静态变量(因为这些变量不需要)
四、Block导致循环引用
我们知道当两个对象互相强引用(即:循环引用)时,就会导致内存泄露。block就很容易导致循环引用,如:当前ViewController拥有一个block的属性(VC强引用block),当我们需要在block花括号里使用self时,就会导致block对self的强引用,从而导致循环引用。
1、MRC时代
ARC模式还没出来的时候,只能用__unsafe_unretained
和__block
来防止循环引用,如下:
__unsafe_unretained typedof(self) unSafeSelf = self;
void (^aBlock) = ^{
[unSafeSelf someMethod]; // 不安全,应该判一下空(就很麻烦了)
}
aBlock();
__block typedof(self) blockSelf = self; // 强引用
void (^aBlock) = ^{
[blockSelf someMethod];
blockSelf = nil; // 将该强引用置空(这一句不能少)
}
aBlock(); // block必须调用一次,否则blockSelf无法释放
都比较麻烦,不方便~
2、ARC时代
ARC模式出来后就有了weak,这时应该使用__weak
来避免循环引用:
__weak typedof(self) weakSelf = self; // 弱引用
void (^aBlock) = ^{
// [weakSelf someMethod]; // 直接使用是有风险的,当此方法执行到一半时,若self被销毁,则会导致crash
__strong typedof(weakSelf) strongSelf = weakSelf; // 强引用一下(此强引用指针作用域只在该block内)
[strongSelf someMethod]; // 若此时self已被释放,则给nil发送消息不会报错
// 若此时self未被释放,则strong指针会保证其引用计数在block执行完之前大于0
// block执行完,strong指针会被释放
}
aBlock(); // 不是必须得执行,因为block里是弱引用
五、面试题
1、__block
修饰基本数据类型后其内存地址?
如下代码,a在block代码之前的地址、在block内部的地址、在block代码之后的地址,都一样么?
__block int a = 10;
NSLog(@"%p", &a);
void(^aBlock)(void) = ^{ // a从stack拷贝到heap
NSLog(@"%p", &a);
};
NSLog(@"%p", &a);
aBlock();
结果是:在block代码之前是一个地址,block代码之后和之内的是另一个地址。因为block初始化的时候会把在栈区的a的结构体拷贝到堆区,访问也都是a->forwarding->a
,因而地址变了。
2、block里弱引用和强引用调用方法的区别?
问Block里面 [weakSelf method] 和 [strongSelf method] 的区别?
__weak typedof(self) weakSelf = self; // 弱引用
void (^aBlock) = ^{
__strong typedof(weakSelf) strongSelf = weakSelf;
[weakSelf someMethod];
[strongSelf someMethod];
}
用weakSelf:当someMethod执行到一半self突然被释放会crash
用strongSelf:会等block执行完之后,self才会被释放(上面 四-2.ARC时代
也提到了)
3、Block从栈拷贝到堆的时机?
- block调用copy函数时
- block作为函数返回值时
- block赋值给
__strong
修饰的id类型/block类型成员变量时 - block作为
Cocoa API
中的方法含有usingBlock
的方法参数时 - block作为GCD API的方法参数时
- block访问了alloc对象时
4、Block和方法捕获参数的区别?
Block捕获参数上面已经说了。方法捕获参数分两种:
-
传值:
形参和实参占不同内存单元,传递的实际上是实参变量的一个拷贝副本,形参的值发生变化也不会传回给实参,是单向传递。
-
传地址:
传递的是实参变量地址的拷贝值,而不是实参变量的值,在函数中对地址所指对象的操作会改变实参的值。但是形参的内容(即存放的实参变量地址)并不会改变。
5、Block跟函数指针有什么联系?
Block是包装了函数指针的对象
6、Block里面使用成员变量会怎样
实际上是:self->(成员变量名),会导致强引用
参考:
OC中block的底层实现原理 (参考结构图)
重识Objective-C:Block底层实现 (参考reserved属性意义)
iOS底层原理总结 - 探寻block的本质(一)(参考结构图)
iOS 底层原理总结:探寻 block 的本质(二)(assign和dispose函数)
Clang Documentation(__block引用变量类型枚举)
iOS中__block 关键字的底层实现原理(不能直接改变值的原因)
iOS-Block底层实现原理(解决循环引用)
说说OC参数传递的那些坑(方法传参)