1.什么是block?
block是将函数及其执行上下文封装起来的【对象】。
int age = 10;
void (^block)(void) = ^ {
NSLog(@"this is a block: %d", age);
};
上面的block在编译阶段,会被编译成如下的_block_impl
结构体:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc; # block的描述信息,内部记录着block的size 所占内存大小
int age; # 捕获的外部变量
}
其中,__block_impl
结构体记录着block内部函数执行的起始地址:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr; # 记录着block内部函数执行的起始地址
}
2.block对【基本数据类型】的捕获
2.1 block对基本数据类型 局部变量的捕获:
局部变量分为:
- 局部自动变量
首先,我们在使用局部变量的时候,会默认省略auto
关键字,auto
关键字表示自动变量,即该变量离开当前作用域后,就会自动销毁:
{
int age = 10;
# 相当于: auto int age = 10;
}
- 局部静态变量
{
static int age = 10;
}
对基本数据类型 局部变量的捕获:
- 对于局部自动变量(
auto
),block会捕获到内部,是值传递:
int age = 10;
void (^block)(void) = ^ {
NSLog(@"age is: %d", age);
};
编译后为:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
int age;
}
外部值的更改,不影响block内部值。
- 对于局部静态变量(
static
), block会捕获到内部,是指针传递:
static int age = 10;
void (^block)(void) = ^ {
NSLog(@"age is: %d", age);
};
编译后为:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
int *age;
}
外部值的更改,影响block内部值。
2.2 block对基本数据类型 全局变量的捕获:
{
int _age = 10;
static int height = 20;
}
- (void)func {
void (^block)(void) = ^ {
NSLog(@"%d, %d", _age, _height);
};
}
对于全局变量,blokc不捕获,直接访问。
总结: block对于基本数据类型 变量的捕获
1).对于局部auto变量,采用值传递的方式捕获到block内部
2).对于局部static变量,采用指针传递的方式捕获到block内部
3).对于全局变量和全局静态变量,不捕获,直接访问
3.block对 对象数据类型 的捕获机制
当block内部访问了对象类型(NSOject
)的auto
变量时,
- 如果block在栈上,将不会对
auto
变量产出强引用 - 如果block是被拷贝到堆上,
- 会调用block内部的
copy
函数 -
copy
函数内部会调用_Block_object_assign
函数 -
_Block_object_assign
函数会根据auto
变量自身的修饰符(__storng、__weak、_unsafe_unretained
),做出相应的操作,类似于retain
,形成强引用或者弱引用
- 会调用block内部的
- 如果block从堆上移除
- 会调用block内部的
dispose
函数 -
dispose
函数会调用_Block_object_dispose
函数 -
_Block_object_dispose
函数会自动释放引用的auto
变量,类似于release
- 会调用block内部的
4.block的种类
block有三种类型,可通过class
方法或者isa指针查看具体类型,均继承自NSBlock
类型,NSBlock
继承自NSObject
:
-
__NSGlobalBlock__
(即_NSConcreteGlobalBlock
),存放于数据data区域,没有访问auto
变量 -
__NSStackBlock__
(即_NSConcreteStackBlock
),存放于栈区,访问了auto
变量 -
__NSMallocBlock__
(即_NSConcreteMallocBlock
),存放于堆区,__NSStackBlock__
调用了copy
或者strong
三种block使用copy后的结果:
-
_NSConcreteGlobalBlock
使用copy
后,什么也不做 -
_NSConcreteStackBlock
使用copy
后,从栈区拷贝到堆区 -
_NSConcreteMallocBlock
使用copy
后,引用计数增加
注意:
在arc下,编译器在一些情况下会默认对block进行拷贝操作:
- block作为函数返回值时
- 将block赋值给
__strong
指针时 - Cocoa API的方法中,含有
usingBlock
的方法参数时 - block作为
GCD
API的方法参数时
【知识补充】修饰符__block
详解
一般情况下,对block截获的变量进行赋值操作时,需要给外部变量添加__block
修饰符。
如下一个普通的block:
int age = 10;
void (^block)(void) = ^ {
NSLog(@"age is: %d", age);
};
⬇️编译后⬇️
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
int age;
}
在添加了__block修饰符
后:
__block int age = 10;
void (^block)(void) = ^ {
age = 20;
};
⬇️编译后⬇️
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
__Block_byref_age_0 *age;
}
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
}
会产生一个__Block_byref_age_0
结构体
- 该结构体的
age
保存着外部变量age
的值,在block内部修改age
值的时候,也是修改此结构体的age
值; - 该结构体还有一个
__Block_byref_age_0
的指针,指向__Block_byref_age_0
结构体自身 - 访问
__Block_byref_age_0
结构体的age
值,是访问__main_block_impl_0
结构体的age
结构体的__forwarding
指针指向的对象的age
字段,即age->__forwarding->age
需要添加__block
修饰符:
- 局部基本数据类型的变量
- 局部对象类型的变量
不需要添加__block
修饰符:
- 局部静态变量
- 全局变量
- 全局静态变量
注意
赋值操作不等于使用,比如在block内部使用外部变量NSMutableArray
的addObject
方法时候,就不需要给外部的NSMutableArray
添加__block
修饰符
使用__block
后的内存管理跟block捕获对象的内存管理类似:
相同点:
- 如果block在栈上,将不会对使用
__block
产生的变量进行引用 - 如果block是被拷贝到堆上,会调用block内部的
copy
函数 - 如果block从堆上移除,会调用block内部的
dispose
函数
不同点:
- 堆上的block捕获对象,会根据外部对象的修饰符来决定是强引用还是弱引用
- 而使用了
__block
后,堆上的block捕获对象,ARC下一定是强引用,MRC下不会强引用
__forwarding
指针作用:
使用__block
后生成的对象(结构体),为什么内部会存在一个指向自身的__forwarding
指针?
栈上block的
__forwarding
指针指向自身栈上block拷贝到堆上后,栈上block的
__forwarding
会指向堆上的block,堆上block的__forwarding
会指向自身
这样设计,保证了无论是在堆上还是在栈上访问,修改的都是同一个值。
常见面试题:
面试题1:
__block int mul = 10;
_block = ^int (int num) {
return num * mul;
};
mul = 6;
[self execulteBlock];
}
- (void)execulteBlock {
int result = _block(4);
思考 result的值
}
答案:24
在栈上通过__block修饰后,创建的局部变量就变成了对象,所以multiplier=6
这一步赋值其实是变成了通过对生成的对象的__forwarding指针,对其成员变量multiplier进行赋值。_blk = ^...
这一步操作,是对block进行了拷贝,在堆上同时生成一份拷贝。拷贝后的multiplier=6
这一步,就变成了对堆上的数据操作。包括后续的executeBlock
都是对堆上的block对象取值操作。
面试题2:以下方式使用__block,会产生循环引用吗?
__block MyBlock *myBlock = self;
_block = ^int(int num) {
return num * myBlock.var;
};
_block(3);
答案:
- MRC下不会产生循环引用
- ARC下会产生循环引用
ARC下需要手动解环,修改如下:
__block MyBlock *myBlock = self;
_block = ^int(int num) {
int result = num * myBlock.var;
myBlock = nil; # 解环操作
return result;
};
_block(3);
⚠️⚠️⚠️注意:这种方法,需要执行block才可以破环,否则无法断开。