花几分钟看看下面的三个子问题,写下你的答案。在完成这篇博文之前,我给了三个朋友回答这三个小问题。最好的结果,除了一位朋友3道题都答对了,其他两位朋友只答了1道题。说明还有很多iOS小伙伴对Block的了解还不够透彻。这篇博文将详细解释Block。1Block使用的简单规则首先了解简单规则,然后分析原理和实现:在Block中,Block表达式截取使用的自动变量的值,即保存自动变量的瞬时值。修改为__block的变量在捕获时不再获得即时值。至于为什么,后面会继续讲。2Block的实现Block是一个带有自动变量(局部变量)的匿名函数。块表达式很简单,一般可以描述为:“^返回值类型参数列表表达式”。但是Block并不是Objective-C独有的语法。怎么了?clang编译器为程序员提供了理解Objective-C背后机制的途??径,通过clang转换可以看出Block的实现原理。通过clang-rewrite-objcyourfile.mclang会将Objective-C代码转换成C语言代码。2.1Block基本实现分析用Xcode创建命令行工程,编写如下代码:intmain(intargc,constchar*argv[]){void(^blk)(void)=^{NSLog(@"Block")};blk();return0;}用clang转换:上面是转换后的代码,不要方块,一段一段看吧。可以看出Block里面的内容被转换成了一个普通的静态函数__main_func_0。看其他部分:main.cpp__block_impl:struct__block_impl{void*isa;intFlags;intReserved;void*FuncPtr;};__block_impl结构体包括一些flags,为以后版本升级保留的变量,以及函数指针。main.cpp__main_block_desc_0:staticstruct__main_block_desc_0{size_treserved;size_tBlock_size;}__main_block_desc_0_DATA={0,sizeof(struct__main_block_impl_0)};__main_block_desc_0该结构包括为未来版本升级保留的变量和块大小。main.cpp__main_block_impl_0:__main_block_impl_0结构包含两个成员变量,分别是__block_impl和__main_block_desc_0实例变量。此外,它还包含一个构造函数。在main函数中调用构造函数如下:main.cpp__main_block_impl_0构造函数调用:void(*blk)(void)=((void(*)())&__main_block_impl_0((void*)__main_block_func_0,&__main_block_desc_0_DATA));去掉各种强制转换,简化:main.cpp__main_block_impl_0构造函数调用简化:struct__main_block_impl_0tmp=__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA);struct__main_block_impl_0*blk=&tmp;上面的代码是:将__main_block_impl_0结构体的指针赋值给实例_main_blockblock_block_0变量结构指针类型。这是我们的初始结构定义:void(^blk)(void)=^{NSLog(@"Block");};另外,main函数中还有一段:((void(*)(__block_impl*))((__block_impl*)blk)->FuncPtr)((__block_impl*)blk);去除各种变换:(*blk->impl.FuncPtr)(blk);其实就是原来的:blk();本节所有代码在block_implementation(https://github.com/summertian4/iOS-ObjectiveC/tree/master/ObjcMemory/ObjcMemory-Test-Code/block_implementation)2.2Block拦截外部瞬时值的实现变量参数Block的声明和调用已经通过clang进行了转换。接下来看一段代码“拦截自动变量”(可以使用命令clang-rewrite-objc-fobjc-arc-fobjc-runtime=macosx-10.7main.m):intmain(intargc,constchar*argv[]){intval=10;constchar*fmt="val=%d\n";void(^blk)(void)=^{printf(fmt,val);};val=2;fmt="这些值被改了,val=%d\n";blk();return0;}clang转换后:对比2.1节的转换代码,可以发现多了一些代码。首先,__main_block_impl_0多了一个变量val,val的赋值加入到构造函数的参数中:int_val,intflags=0):fmt(_fmt),val(_val){impl.isa=&_NSConcreteStackBlock;impl.Flags=flags;impl.FuncPtr=fp;Desc=desc;}};而在main函数中,Block的声明就变成了这句话:main.cpp__main_block_impl_0constructorcall:void(*blk)(void)=((void(*)())&__main_block_impl_0((void*)__main_block_func_0,&__main_block_desc_0_DATA,fmt,val));删除转换:main.cpp__main_block_impl_0构造函数调用简化:struct__main_block_impl_0tmp=__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,val);struct__main_block_impl_0*blk=&tmp;_main_block_impl_0的内部变量被保留。无论声明后val的值如何变化,都不会影响调用Block时访问到的内部val值。这就是Block捕获变量瞬时值的方式。_本节所有代码在EX052.3__块变量访问实现中分析我们知道块可以被读取,但是局部变量不能被改变。如果你更改它,Xcode会提示你不能更改块内的变量。Block内部只对局部变量只读,但是Block可以读写以下变量:静态变量静态全局变量全局变量的意思就是下面的代码没有问题:intglobal_val=1;staticintstatic_global_val=2;intmain(intargc,constchar*argv[]){staticintstatic_val=3;void(^blk)(void)=^{global_val=1*2;static_global_val=2*2;static_val=3*2;}return0;}如果你想在Block内部写入局部变量,需要对访问的局部变量加上__block修饰。__block修饰符其实类似于C语言中的static、auto、register修饰符。用于指定将变量值设置到哪个存储域。在特定的__block之后进行了哪些更改?我们可以写代码测试:EX07:intmain(intargc,constchar*argv[]){__blockintval=10;void(^blk)(void)=^{val=1;};return0;}clang转换后:与2.2相比,似乎添加了额外的代码。又发现了两个结构。main.cpp__Block_byref_val_0:struct__Block_byref_val_0{void*__isa;__Block_byref_val_0*__forwarding;int__flags;int__size;intval;};令人惊讶的是,块类型的val成为结构Block_byref_val_0的实例。这个例子包含了isa指针,一个标志位flags,和一个记录大小size。最重要的是,多了一个转发指针和val变量。这是怎么回事?在main函数部分,结构体被实例化:main.cppmain.m部分:__Block_byref_val_0val={(void*)0,(__Block_byref_val_0*)&val,0,sizeof(__Block_byref_val_0),10};我们可以看到,结构体对象初始化时:__forwarding指向结构体实例本身在内存中的地址val=10而在main函数中,val=1的赋值语句变为:main.cppval=1;对应函数(val->__forwarding->val)=1;这里我们可以看出它的本质,val=1,其实改变的是__Block_byref_val_0结构体实例val中的__forwarding指针(也就是它本身)的val变量。val访问也是如此。可以理解为取地址改变变量的值,类似于C语言中取地址改变变量。因此,可以更改块中声明的变量。至于转发的其他大作用,我们会继续分析。本节代码中EX05中Block3的三种存储域分别是:————堆中出现__NSConcreteGlobalBlock的地方是:当设置全局变量的地方有Block语法时,当Block语法的表达式不使用任何外部变量时,栈上设置的Block,如果它所属的变量的作用域结束时,该块将被丢弃。如果其中使用了块,则块所属变量的作用域也将被丢弃。为了解决这个问题,需要在必要的时候将Block从栈中移到堆中。当ARC有效时,很多情况下,编译器会帮助完成Block的拷贝,但很多时候,我们需要手动拷贝Block。复制不同存储域的Block时,影响如下:复制时,对访问的__block类型对象的影响如下:此时,我们可以看到__forwarding的巨大作用——不管Block是否在此时堆或栈中,由于__forwarding指向的是结构体实例转换为局部变量的真实地址,所以保证了正确访问。具体来说:当一个块变量被一个Block使用时,这个Block从栈中复制到堆中,块变量也被复制到堆中并被Block持有。当一个block变量被多个Block使用时,当任意一个Block从栈中复制到堆中时,该block变量也会被复制到堆中并被Block持有。但是由于__forwarding指针的存在,不管block变量和Block是否在同一个存储区,都可以正确访问到block变量。如果堆上的一个Block被丢弃,它使用的__block变量也将被释放。上面说了编译器会帮忙完成一些block的copy,也有手动copyblock的。然后将Block复制到堆中(本段摘自《Objective-C高级编程iOS和OSX多线程与内存管理》):当调用Block的copy方法时,将Block作为返回值时,在方法名中包含usingBlock的Cocoa框架方法或GCDAPI中传递Block时,Block被赋值给附加的__strong修饰符成员变量(id类型或Block类型)4块循环引用块循环引用是编程中很常见的问题,甚至很多时候,我们并不知道发生了循环引用。直到有一天我们突然发现“为什么这个对象没有调用delloc”,我们才意识到有问题。在《块存储域》中也有说明,块在复制后会保留一次__block对象。那么以下几种情况会出现循环引用:];_blk=^{NSLog(@"self=%@",self);};returnsself;}-(void)dealloc{NSLog(@"%@dealloc",self.class);}@endintmain(intargc,constchar*argv[]){idmyobj=[[MyObjectalloc]init];NSLog(@"%@",myobj);return0;}因为self->blk,blk->self无法释放。但需要注意的是,以下情况也会出现循环引用:block_retain_cycle@interfaceMyObject:NSObject@property(nonatomic,copy)blk_tblk;//下面多了一句@property(nonatomic,strong)NSObject*obj;@end@implementationMyObject-(instancetype)init{self=[superinit];//下面加一句_blk=^{NSLog(@"self=%@",_obj);};returnsself;}-(void)dealloc{NSLog(@"%@dealloc",self.class);}@endintmain(intargc,constchar*argv[]){idmyobj=[[MyObjectalloc]init];NSLog(@"%@",myobj);return0;这是由于self->obj,self->blk,blk->obj。这种情况很容易被忽视。5问题重审先看前几个子题:***问题:由于Block捕获的是瞬时值,输出在blockval=0第二个问题:由于val是__block,外部变化会影响内部访问,所以输出在blockval=1第三个问题:和第二个问题类似,val=1会影响Block的内部访问,所以先在blockval=1输出,然后改变里面的val值block,blockval=2后再次访问时输出。其他这篇文章是看了《Objective-C高级编程iOS和OSX多线程与内存管理》一书后写的。和内存管理”。强烈推荐给大家这本书。这本书记录了iOS内存管理的深入内容。但是需要注意的是,这本书中的很多知识点都不是很详细,需要你以扩展的心态来学习.在解释不详细的地方,主动去探索,去扩展,去寻找更多的资料。最后,你会发现你对iOS内存管理有了更深入的理解。对于文章中的测试代码,都在(https://github.com/summertian4/iOS-ObjectiveC/tree/master/ObjcMemory)中。
