底层分析
-
结论:
- Block本质上也是一个OC对象,内部也有个isa指针,且最终父类也是(NSObject)
- 定义:Block是封装了函数调用以及函数调用环境的OC对象
-
底层结构:
//OC分析够的Block结构 struct __Person__blockFuncationF_block_impl_0 { struct __block_impl impl; //定义如下 struct __Person__blockFuncationF_block_desc_0* Desc; //定义如下 //捕获到内部的变量 int a; NSString *name; int *b; NSString **nameS; //析构函数,相当于OC的init函数 __Person__blockFuncationF_block_impl_0(void *fp, struct __Person__blockFuncationF_block_desc_0 *desc, int _a, NSString *_name, int *_b, NSString **_nameS, int flags=0) : a(_a), name(_name), b(_b), nameS(_nameS) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; //impl的结构体定义 struct __block_impl { void *isa; //isa指针 int Flags; int Reserved; void *FuncPtr; //指向函数实现的指针 }; //描述的定义 struct __Person__blockFuncationF_block_desc_0 { size_t reserved; size_t Block_size; //闭包的大小 }
-
图解:
闭包如何使用局部变量、静态局部变量和全局变量?
变量类型 | 捕获到Block内部 | 访问方式 |
---|---|---|
局部变量 | 是 |
值传递(浅拷贝) |
静态局部变量 | 是 |
指针传递 |
全局变量 | 否 |
直接访问 |
说明:对于局部变量(自动变量auto),闭包里保存瞬时值,在闭包范围后改变局部变量的值不会影响Block内部值得变化。
注意:
对于对象变量,(比如:
NSString *name = @""
,直接传name
是传递指向的堆内存指针值,而&name
则是自身所在内存的指针)-
特别说明:静态局部变量,Block捕获时就用的是
&变量
,然后内部申明相应变量指针将其存起,从而内部可以修改其值。对于局部变量(自动变量auto):闭包里保存瞬时值,并且是值传递
a.基础变量:直接捕获其值(内存值),在闭包之后改变,闭包内部不会有任何变化;
b.不可变对象变量:捕获其值(内存值),在闭包之后改变(重新赋值,相当于改变了其指针指向,但闭包内部捕获时强引用故对象还存在,闭包内部不会有任何变化);
c.可变对象变量:捕获其值(内存值),在闭包之后改变(重新赋值,相当于改变了其指针指向,但闭包内部捕获时强引用故对象还存在,闭包内部不会有任何变化,但自身增删改,而指针未变,指向的值块也跟着变,则闭包内部的值块也会跟着改变);
静态局部变量:捕获其指针值,在闭包之后改变(闭包内部会跟着变,因为静态变量,自初始话~销毁,指针都不会改变)(注:初始化一个全局变量或static变量时,只能用常量赋值,不能用变量赋值)
全局变量:不会捕获到内部,而是直接访问。故在闭包之后改变,闭包内部也会跟这变化
特殊:对于方法内部的 self,也是自动变量
因为:OC方法转换为C语言函数时,会默认转换为如下格式:
返回值 方法名(类 *self, SEL _cmd) {}
故:self 和 _cmd 也是auto变量
ARC下,Block的类型和使用变量的关系图?
- Block类型
- 继承顺序:类型 -> NSBlock -> NSObject
Block类型 | 环境 | 存储环境 | copy效果 |
---|---|---|---|
__NSGlobalBlock__ |
没有访问任何auto变量 | 数据区域 | 什么也不做 |
__NSMallocBlock__ (MRC下为__NSStackBlock__ ) |
访问了auto变量 | 堆(MRC下为堆) | 引用计数+1(从栈复制到堆) |
__NSMallocBlock__ |
上面Malloc/Stack调用了copy | 堆 | 引用计数+1 |
__block
可以用于解决block
内部无法修改auto变量值
的问题
__block
不能修饰全局变量、静态变量(static)
编译器会将__block
变量包装成一个对象
调用的是,从__Block_byref_age_0
的指针找到age
所在的内存,然后修改值
总结一下在Block中改变变量值有2种方式,一是传递内存地址指针到Block
中(比如用:static 静态局部变量),二是改变存储区方式(__block
)。
1 传递内存地址:
对于静态对象变量
,在被Block
捕获后,在Block
的结构体实例变量会增加一个指针用来存储外部传递&j静态变量
的本身指针,内部通过此指针即可改变其值。-
2
__block
改变存储方式:
__block
修饰自动变量后,__block
的变量也被转化成了一个结构体:__Block_byref_i_0
,这个结构体有5个成员变量。struct __Block_byref_i_0 { void *__isa; 指针 __Block_byref_i_0 *__forwarding; 指向自身类型的__forwarding指针 int __flags; 标记flag int __size;大小 int i; 变量值 ( 基础变量都是强引用、 ARC:对象类型的会根据 auto变量的修饰符进行assign MRC:对象类型的都是弱引用 ) };
-
图解如下:
ARC下,哪些情况会自动进行Copy操作?
- 当
Block
作为函数返回值时; - 当
Block
被__strong指针
引用时(ARC下,auto变量会自动添加__strong); - 在Cocoa框架下,方法名含有
usingBlock
的方法参数时; -
Block
作为GCD API的方法参数时。 - 建议写法:
- MRC下,
@property (copy, nonatomic) void (^block)(void);
- ARC下,
@property (strong, nonatomic) void (^block)(void); 和 @property (copy, nonatomic) void (^block)(void);
效果相同,都会拷贝到堆上去。
- MRC下,
循环引用
-
典型错误
- 对象本身拥有Block,Block内部又强引用对象本身。
- 注意:如果闭包内部使用自身的强引用的
_成员变量
,同样会产品循环引用,因为内存会捕获__strong self
从而去找到相应的成员变量。 - (懒加载则不会):因为
_成员变量
,没有调用setter方法。
- 注意:如果闭包内部使用自身的强引用的
- 对象本身拥有Block,Block内部又强引用对象本身。
-
解决方法(ARC)
-
__weak
:不会产生强引用,指向的对象销毁时,会自动让指针置为nil.(推荐) -
__unsafe_unretained
:不会产生强引用,但不安全(因为指向的对象销毁是,指向存储的地址不变,如果再次访问,将产生野指针错误)(不太推荐) -
__block
:把对象用__block
修饰(则会编译成struct对象,内部并强引用),同时必修调用Block,且Block内部把对象置为nil.(麻烦,不推荐,了解)
-
-
解决方法(MRC)
-
__unsafe_unretained
:同上 -
__block
:这不相同,因为在MRC下,__block
特殊不会对对象进行强引用,故是弱指针,则根本就不会造成循环引用。
-
Block知识总结
- 当
block
在栈上时,对它们都不会产生强引用 - 当
block
拷贝到堆上时,都会通过copy
函数来处理它们 -
__block
变量(假设变量名叫做a)
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
- 对象类型的auto变量(假设变量名叫做p)
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
- 当block从堆上移除时,都会通过dispose函数来释放它们__block变量(假设变量名叫做a)
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
- 对象类型的auto变量(假设变量名叫做p)
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
-
图解如下:
面试题
1、 block的原理是怎样的?本质是什么?
本质:Block俗称闭包,是一个匿名函数,本质是封装了函数调用以及调用环境的OC对象。
原理:block内部或捕获其内部使用到的auto变量和静态变量,使其延时调用时依然能使用其外部变量。
2、 __block
的作用是什么?有什么使用注意点?
作用:使其block内部能修改外部的auto变量。
注意点:不要滥用,不能用于修饰静态变量。
3、 block的属性修饰词为什么是copy?使用block有哪些使用注意?
在ARC环境下,不一定费用copy,可用copy和strong,效果一样都会拷贝到堆上。
使用注意:避免循环引用问题,和使用弱引用的对象时提前释放内存问题。
4、 block在修改可变的对象时,需不需要添加__block
?
不需要,因为是值传递,Block内部会自动生成对象来引用外部可变变量,所以此对象的增删改是可以的,但不能改变其指针指向(因为相当于在另一个函数内部修改另一个函数内部的局部变量的指针指向,是不能的)。