序言:翻阅资料,学习,探究,总结,借鉴,谢谢探路者,我只是个搬运工。
参考、转发资料:
http://www.cnblogs.com/chenxianming/p/5554395.html
http://www.jianshu.com/p/ca6ac0ae93ad#
书籍:Objective-C高级编程
1. Block的本质
Block的本质其实是object对象
2. 内部分析
在百度到的知识基本上都是使用clang(LLVM编译器,和GCC类似)来解析编译的。总结一下自己的理解。
以下代码为例:
int main(int argc, char * argv[]) {
void (^test)() = ^(){
};
test();
}
接下来我要用到一个命令clang src.m -rewrite-objc -o dest.cpp.这个意思是用clang编译器对源文件src.m中的objective-c代码转换成C代码放在dest.cpp文件。其实xcode编译时也会帮我们转换。我们这样就可以dest.cpp在看到我们定义和调用的block转换成C是怎么样的。执行命令后查看这个dest.cpp会发现有一大堆代码。下面我把对我们有用并能够说清楚原理的关键贴上来并加以注释:
//__block_imp: 这个是编译器给我们生成的结构体,每一个block都会用到这个结构体
struct __block_impl {
void *isa; //对于本文可以忽略
int Flags; //对于本文可以忽略
int Reserved; //对于本文可以忽略
void *FuncPtr; //函数指针,这个会指向编译器给我们生成的下面的静态函数__main_block_func_0
};
/*__main_block_impl_0:
是编译器给我们在main函数中定义的block
void (^test)() = ^(){
};
生成的对应的结构体
*/
struct __main_block_impl_0 {
struct __block_impl impl; //__block_impl 变量impl
struct __main_block_desc_0* Desc; //__main_block_desc_0 指针,指向编译器给我们生成的结构体变量__main_block_desc_0_DATA __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { //结构体的构造函数
impl.isa = &_NSConcreteStackBlock; //说明block是栈blockimpl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;}
};
//__main_block_func_0: 编译器根据block代码生成的全局态函数,会被赋值给impl.FuncPtr
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
}
//__main_block_desc_0: 编译器根据block代码生成的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)}; //这里就生成了__main_block_desc_0的变量__main_block_desc_0_DATA
//这里就是main函数了
int main(int argc, char * argv[]) {
// 以下代码转换之后,void (* test)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
void (*test)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); //下面单独讲
// 以下代码转换之后,test->FuncPtr(test);
((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test); //下面单独讲
}
- 总结:
好了讲到这里,就可以进行一个中途简单性的总结:忽略中间的复杂分支,留下主线,当我们声明一个Block变量a并为它赋值时,其实就是创建一个函数指针ptrA,再根据Block a赋值的代码生成一个静态函数,而指针ptrA就指向这个静态函数。Block a调用时就是使用函数指ptrA调用生成的静态函数。
3. 获取自动变量的瞬间值
- block的定义:带有自动变量的匿名函数。然而怎么获取自动变量的瞬间值呢?
我们对最初的代码进行修改添加变量
int main(int argc, char * argv[]) {
int value = 1;
void (^test)() = ^(){
int valueTest = value;
};
test();
}
如上的内部实现代码:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 这里是不同于上面的,多出来的,但是其中使用的全局变量(包括全局静态变量)不会生成对应的参数对象。只有自动变量才会。
int value;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _value, int flags=0) : value(_value) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int value = __cself->value; // bound by copy
int valueTest = value; // 这里是不同于上面的,多出来的
}
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)};
int main(int argc, char * argv[]) {
int value = 1;
void (*test)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, value));
((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);
}
- 理解一下:在Block外定义的自动变量(Block中不使用的自动变量不做处理),将会随着Block的创建,以形参的形式传递进Block,在创建对应Block方法的时候,结构体中会生成对应形参的变量用于储存记录形参的值,通过构造方法赋值。
你会发现,在block生成的匿名函数中,block是用定义的变量去接收自动变量的值并存储,在使用block生成的静态函数static void __main_block_func_0(struct __main_block_impl_0 *__cself)
时,传入了是Block对象,通过拿取的是Block中的值。
3. __block 的实现原理(下一模块就再对它的作用情况进行细致的讲解)
以下代码为例:
int main(int argc, char * argv[]) {
__block int value = 1;
void (^test)() = ^(){
value = 2;
};
test();
int value1 = value;
}
一如既往,我们看一下添加了_block内部发生了什么变化。
//根据带__block修饰符的变量value,编译器给我们生成了个结构体
struct __Block_byref_value_0 {
void *__isa;
__Block_byref_value_0 *__forwarding; //这个会指向被创建出来的__Block_byref_value_0实例
int __flags;
int __size;
int value;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_value_0 *value; //保存__Block_byref_value_0变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_value_0 *value = __cself->value; // bound by ref
// 注意这里
(value->__forwarding->value) = 2;
}
//这两个函数分别会在test block 被拷贝到堆和释构时调用的,作用是对__Block_byref_value_0实例的内存进行管理,至于怎么管理,这里就不讨论了,这里就会调用上面导出来的接口。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->value, (void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->value, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*); //回调函数指针,会被赋值为__main_block_copy_0
void (*dispose)(struct __main_block_impl_0*); //回调函数指针,会被赋值为__main_block_dispose_0
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0}; /*{ 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0},这句就是创建一个例的意思,这是结构体的一种构造方式。*/
int main(int argc, char * argv[]) {
/*我们定义的__block int value转换后并不是一个简单的栈变量,而会是新建的__Block_byref_value_0堆变量*/
__attribute__((__blocks__(byref))) __Block_byref_value_0 value = {(void*)0,(__Block_byref_value_0 *)&value, 0, sizeof(__Block_byref_value_0), 1};
void (*test)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_value_0 *)&value, 570425344));
((void (*)(__block_impl *))((__block_impl *)test)->FuncPtr)((__block_impl *)test);
//最后面使这句int value1 = value;使用value时,在我们表面看到是好像是使用main函数里的一个局部栈变量,其实不是,使用的是堆里面的容int value1 = (value.__forwarding->value);
}
- 总结:我们不能看出,使用了_block就多了修饰value的结构体
_Block_byref_value_0
,而最根本的原因是用了_block修饰的变量被从栈区copy到堆区中了,这样,就不会被立即释放了(而static修饰的静态变量存储在静态区、全局变量不需要用_block修饰是一个道理)。 - 注意:其中有个细节需要理解,为什么在
__Block_byref_value_0
的结构体中还有一个一样__Block_byref_value_0
结构体变量。其实是有原因的,__Block_byref_value_0
中的_forwarding有什么作用?_forwarding,指向自己的指针,当从栈copy到堆时,指向堆上的block,这也是为什么(value->_forwarding->value) = 2
反复指向的原因。
_forwarding 的原理如下图
4. Block的存储区域
- Block和_block变量的实质
- Block:栈上Block的结构体实例。
- _block:栈上_block变量的结构体实例。
- Block的类:
- _NSConcreteStackBlock 栈区
之前我们使用的都是_NSConcreteStackBlock(栈区)类型的Block。 - _NSConcreteGlobalBlock 数据区(静态区)
_NSConcreteGlobalBlock稍微解释一下:在记述全局变量的地方使用Block语法时,生成的Block。
满足条件:- 记述全局变量的地方有Block语法时。
- Block语法的表达式中不使用应截获的自动变量时。
例如:
void (^blk)(void) = ^(printf("HelloWorld")) ;
int main(){
}
- _NSConcreteMallocBlock 堆区
Blocks提供了将Block和_block变量从栈上赋值到堆上的方法来解决这个问题。将配置在栈上的Block赋值到栈上,这样即使Block语法计数的变量作用域结束,堆上的Block还可以继续存在,
例子:
/**
* _Block_copy函数
* 将栈上的Block复制到堆上
* 复制后,将堆上的地址作为指针赋值给变量tmp
*/
tmp = _Block_copy(tmp) ;
注意:Block从栈区copy到堆区是相当消耗CPU的。
不同类型Block执行_Block_copy的图标说明。
Block的类 | 副本源的配置存储域 | 赋值效果 |
---|---|---|
__NSConcreteStackBlock | 栈 | 从栈区复制到堆区 |
__NSConcreteGlobalBlock | 程序的数据区域(静态区) | 什么都不做 |
__NSConcreteMallocBlock | 堆区 | 引用计数增加 |
5. 具体例子的分析,并用以上讲解的知识解释
- 为什么在Block方法外修改自动变量不会影响Block内部的使用。
以下代码为例:
int value = 0 ;
void (^test)() = ^(){
// 结果:value = 0
NSLog(@"value = %d",value) ;
};
value = 2 ;
test();
Block方法获取的是自动变量的瞬间值。
- 为什么Block不能直接修改自动变量。
以下代码为例:
int value = 0 ;
void (^test)() = ^(){
// 结果:value = 0
value = 4 ;
};
test();
这段代码在编写编译的时候就会报错,因为自动变量值截获只能保存执行Block语法瞬间的值,保存后就不能改写该值。其实这段代码在编写的规范上没有任何错误,但是因为在实现上不能改写被截获自动变量的值,所以当编译器在编译过程中检出给被截获自动变量赋值的操作时,便产生编译错误。
- 虽然在Block中不能对自动变量直接修改,但是能对自动变量做其他操作。
例如:
NSMutableArray *array_m = [NSMutableArray array] ;
[array_m addObject:@1] ;
void (^test)() = ^(){
// 结果array_m: (1,2)
[array_m addObject:@2] ;
NSLog(@"array_m: %@",array_m) ;
};
test();
因为array_m是个指针对象,和Block中value都同时指向同一个类的实例化区域,所以可以在不改变value指针地址的基础上进行操作。但是你要是改变array_m的指针地址例如
array_m = nil 就会出现错误。
- 使用_blcok解决不能修改Block外自动变量的值。
以下代码为例:
__block int value = 0 ;
void (^test)() = ^(){
value = 4 ;
// 结果:value: 4
NSLog(@"value: %d",value) ;
};
test();
我们可以参照上面简述的代码内容:
//根据带__block修饰符的变量value,编译器给我们生成了个结构体
struct __Block_byref_value_0 {
void *__isa;
__Block_byref_value_0 *__forwarding; //这个会指向被创建出来的__Block_byref_value_0实例
int __flags;
int __size;
int value;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_value_0 *value; //保存__Block_byref_value_0变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_value_0 *_value, int flags=0) : value(_value->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_value_0 *value = __cself->value; // bound by ref
// 注意这里
(value->__forwarding->value) = 4;
}
你会发现对了一个value在Block中变成了一个结构体,这就是奥秘,从_main_block_func_0
方法来看,你在Block的操作结构体指针永远都不会变和自动变量一致,而是对_Block_byref_value_0
的value进行操作,所以可以赋值。
- 疑惑点:
为什么自动变量值截获只能保存执行Block语法瞬间的值,保存后就不能改写该值?
首先Block中的value(用来记录的对象我用value统称)对象的地址是不允许改变的。为什么静态变量的方式也适用于自动变量的访问,但是我们没有这样做呢?
实际上,在由Block语法生成的值Block上,可以存在超过其变量作用域的被截获对象的自动变量。变量作用于结束的同时,原来的自动变量被废弃,因此Block超过变量作用域而存在的变量同静态变量一样,将不能通过指针访问原来的自动变量。而这个可以存在指的就是NSConcreteMallocBlock。NSConcreteMallocBlock的访问是通过_forwarding实现的,只要栈上的结构体实例成员变量_forwarding指向堆上的结构体实例,那么不管是在栈上的_block变量还是从堆上的_block变量都能访问。-
为什么局部静态变量不需要任何操作就能够在Block块中使用操作,而自动变量需要?
从广义的角度讲的话,就是和变量在内存当中的存储位置和其作用域。从内部实现原理来说,请看下面代码,转换之后挑取需要的代码:- 自动变量方法
// 方法
int main(int argc, char * argv[]) {
int value = 1 ;
void (^test)() = ^(){
printf(value) ;
};
test();
}
// 转换之后__main_block_impl_0结构体中
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 注意这里 value变量不带*
int value;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _value, int flags=0) : value(_value) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
- 静态局部变量
// 方法
int main(int argc, char * argv[]) {
static int value = 1 ;
void (^test)() = ^(){
printf(value) ;
};
test();
}
// 转换之后__main_block_impl_0结构体中
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 注意这里 value变量带*
int *value;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _value, int flags=0) : value(_value) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
二者的区别:value对象,一个带*是指针,一个不带指针是个基本变量类型。
- 总结:
- 在都是局部变量的情况下(这里不考虑全局变量、全局静态变量,为了在Block块中使用这些变量时,Block内部是不做如何处理都没问题的),我们不能修改赋值定义在Block结构体
_main_block_impl_0
中的变量value(我有时候会用value统称变量名),弄清楚基础变量和指针变量对你理解这句话,甚至对各种情况的执行的理解都有帮助。 - 要明白不做任何处理和修饰的自动变量和Block中的value(Block中用来存储自动变量值的变量,我统称value)都是存储在栈区的基本变量,这两个变量的存储区域的指针地址不同,所以即使我们在Block中修改了value的值,但是也不能修改自动变量。但是不管是static修饰自动变量还是_block修饰自动变量都是_block中存储自动变量的value变成了一个指针变量。所以你修改的指针地址,而是值。
- 要想在Block中操作局部变量,需要保存局部变量和Block中存储变量的指针地址一致,不然不能进行复制操作,但是可以不改变指针地址的操作例如数组添加元素。
- 在都是局部变量的情况下(这里不考虑全局变量、全局静态变量,为了在Block块中使用这些变量时,Block内部是不做如何处理都没问题的),我们不能修改赋值定义在Block结构体
6. Block的循环引用
- 说明:
如果在Block中使用附有_strong修饰符的对象类型自动变量,那么当Block从栈复制到堆区时,该对象为Block所持有,这样容易引起循环引用。
typedef void (^Test)(void);
@interface ViewController ()
{
Test test ;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
test = ^{
NSLog(@"self: %@",self) ;
} ;
}
执行的Block语法使用附有_strong修饰符的id类型变量self,因此通过Block语法生成在栈上的Block此时由栈区复制到堆区,并持有所使用的self。
如图:
接下来解释一下上面一句话:
// 转换之前的代码
NSMutableArray *array_m = [[NSMutableArray alloc]init] ;
void (^test)() = ^(){
[array_m addObject:@(1)] ;
};
test();
// 转换之后的代码,省略部分代码
__main_block_impl_0{
id _strong array_m ;
}
解释:Block中使用的赋值给附有_strong修饰符的自动变量的对象和复制到堆上的_block变量由于被堆上的Block持有,因此可超出其变量作用域而存在(有点复杂,不做解释)。
解决:用_weak修饰self,来解决循环引用。
- 解决方式
- 使用_weak修饰self,来解决循环引用。
typedef void (^Test)(void);
@interface ViewController ()
{
Test test ;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak ViewController *weakSelf = self ;
test = ^{
NSLog(@"self: %@", weakSelf) ;
} ;
}
- 使用_blcok修饰self,来解决循环引用。
typedef void (^Test)(void);
@interface ViewController ()
{
Test test ;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__block ViewController *blockSelf = self ;
test = ^{
NSLog(@"self: %@", blockSelf) ;
// 注意
blockSelf = nil ;
} ;
// 注意
test() ;
}
注意:需要注意两点。
1、一定要在Block中把_block变量置nil 。
2、一定要执行这个Block方法 。
原理图:
- __block的特点
- 通过_block变量可控制对象的持有时间。优点
- 为了避免循环引用必须执行Block。缺点