函数或函数指针+外部上下文变量 = 闭包,block其实是OC对闭包的实现,配合dispatch_queue实现简单的多线程异步。Dispatch_block_t是GCD提供的无参无返回值的Block语法糖。
block的数据结构
struct __block_impl
isa指针:实现对象相关功能,执行时指向&_Stack/Malloc/Global三种类型的地址
flags:附加信息,block copy时会使用(BLOCK_HAS_COPY_DISPOSE、BLOCK_IS_GLOBAL等)invoke:函数指针,指向block实现的函数调用地址
descriptor:附加描述信息(Block_descriptor,包括size、copy和dispose的函数指针,signature函数签名等)
/* variables:block捕获过来的变量(把变量或变量的地址复制到了结构体中) */
实现流程:调用_impl_0构造函数初始化block结构体,赋值回blockTest变量,blockTest->FuncPtr()即执行函数。
/* 具体实现过于复杂的标记 */^{ blockContent } ();clang -rewrite-obj block.c文件,改写观察c语言实现:__block_impl,block结构体指针_impl_0,结构体最终实现,block的入口(__block_impl,Desc两个变量 和 impl_0的构造函数赋值isa、Flags、FuncPtr、Desc)
注:{ impl.isa = &_NSConcreteStackBlock; // 被强引用时应赋值Global类型 }
_desc_0{ reserved; Block_size; } // block描述,当外部有__block变量需要被捕获时,desc中会增加copy和dispose函数指针修改捕获变量的引用计数
_func_0{ 函数实现 } // block指向的函数实现
Block的三种类型
Block有isa指针,所以也有对象的相关功能,有三种类型,_NSConcrete开头(ARC下只有两种)
Global:全局静态block,处于data段,不访问任何外部变量,通过指针安全访问
Stack:保存在栈中,函数返回时(block变量作用域结束)被销毁,__block变量也会废弃,所以提供copy功能拷贝到堆,即引入Malloc类型(ARC下自动拷贝到堆,所以会被Malloc取代)
Malloc:保存在堆中,引用计数为0时销毁;Block被copy时会复制到堆中,并经过很多步才赋值为malloc。
注:写在全局作用域时,不获取任何外部变量时,才是Global类型,否则ARC下自动copy变成Malloc类型,放在堆区。
Block的copy
https://www.jianshu.com/p/eff7130620a9https://www.jianshu.com/p/bf3798fe3f49Block
会被Copy到堆上的4种情况1、手动[copy];2、block是函数返回值;3、被强引用(赋值给__strong或id类型);4、含有usingBlock方法的系统API,所以大多情况下都会是Malloc类型。
作为变量:刚声明时在栈上(如果获取了外部变量),赋值给非weak的变量(被强引用)即copy到堆上。
作为属性:strong/copy修饰的属性(被强引用)会被copy,weak和assign不会。
函数中:作为参不会被copy,作为返回值会被copy(因为被retain)。
自动copy:ARC下,Block先创建在栈上,如果获取了外部变量,会通过objc_retainBlock把block拷贝到堆上,并通过objc_autoreleaseReturnValue把堆上的block放入自动释放池。
注:stack到malloc是深拷贝,malloc到malloc是浅拷贝。
变量的捕获
https://www.jianshu.com/p/90dfdd73f431
基础过程:impl_0结构体中会增加一个同名变量,并在构造函数中以:param(_param)用_param初始化捕获到的成员变量到结构体中保存起来。在最终函数执行的代码func_0中,通过__cself->intValue取出变量。
捕获方式:是值的copy,复制到数据结构中,只能访问,无法修改。
注:上述基础的捕获 局部变量的过程是不能在block内部更改变量的值的,但有三种可以:全局变量extern,静态变量static,全局静态变量extern static。原因:全局变量作用域大,block访问和普通函数访问没有区别;静态变量是指针传递(而非值传递)。
__block(和静态变量一样,指针传递)
原理:被__block修饰符修饰的变量会实现一个__Block_byref_param_0结构体,封装外部变量,保存了变量的引用;还会增加_copy_0和_dispose_0两个方法维护引用计数;所以可以更改外部变量的值。注1:__block会在实现中加一个__Block_byref_i_0结构体指针存捕获的变量的引用,所以会更改外部的值,并且,它会在desc中增加copy和dispose函数来修改引用计数。
注2:__Block_byref_intValue_0结构体储存了外部变量,内部有个__forwarding指针指向自己,copy时会把这个指针指向堆上。
__forwarding:指向捕获变量自身的指针,block拷贝到堆时会指向堆区的捕获变量结构体,用于解决局部变量超出作用范围时block也能访问的问题,而这也是block需要copy到堆上的意义。
block中对象变量的内存管理
https://www.jianshu.com/p/f222dc15b0e4
__block
当捕获的变量obj从栈上拷贝到堆上时,assign被调用,block持有obj;当obj引用计数为0被释放时,dispose被调用,block对obj的引用也消失。所以__block捕获的变量可以对其进行适当的内存管理。
__weak
变量的作用域结束后,内存被释放,block捕获到的变量指向nil,即不会强引用原对象(用于打破和self的循环引用)
注:id array = [[NSArray alloc] init]; blk = [^(id obj) { // blockContent } copy]; // 因为函数结束时,捕获的变量作用域结束,block会被直接销毁,所以要手动copy。
循环引用
原因:block会拷贝对象变量的指针引用计数+1,self被捕获后被强引用,此时如果self也持有block那就会导致循环引用。
解决:外部声明一个self的弱引用weakSelf,block不持有self,打破循环引用。注:__block修饰后,在block内 = nil,也可以释放对变量的强引用打破循环引用,但如果block不调用就内存泄漏了。
与delegate的对比
1、block是让代码块以闭包(一个函数+其执行的外部上下文变量)的形式传递内容,实在是太轻量级了,适用于大多数异步和简单的回调。
2、当有多个方法回调时应当选用delegate会更清晰,如UITableView的delegate代理方法。
3、block会涉及到栈区到 堆区的拷贝等操作,delegate只是定义了一个方法列表,在遵守了协议的对象的objc_protocol_list中添加了一个节点,运行时向对象发送消息即可。所以block在时间空间消耗都大于delegate,性能消耗较大。
4、代理更加面向过程,block更加面向结果。
Block和函数指针的区别
函数指针仅仅是一个地址,不具备函数原型信息,没有类型限制,比如一个指向变量的指针同样可以指向一个函数,但是 block 作为函数对象,是有部分函数信息的,类型限制更明确。
block 方式便于实现真正的 “函数式” 编程,让函数成为基本的运算元,往更远的方向说,真正的函数式语言可以去掉寄存器(请参考冯诺依曼机器基本架构),提高程序的执行效率,近段时间的语言都支持 lambda 语法,包括JS、 C++ 、 Python 、 Ruby等,可见各个编程语言为改进冯诺依曼架构做出的努力和准备。
提高程序的健壮性, 定义函数的代码会位于程序的代码段,如果函数内部出现内存溢出,就会直接导致 crash,因为代码段是不可写的;block 作为函数对象在运行时生成,位于栈内,即使出现内存溢出,一般也不会直接导致 crash。
相关问答
请描述一下Block的底层结构
_block_impl结构体即Block的实现,包括isa,funcPtr,flag,reserved,descriptor,捕获的变量。
Block的三种类型,何时从栈copy到堆
不捕获变量时:_NSConcreteGlobalBlock;刚创建且捕获了变量时在栈上:stack;被强引用或作为函数返回值时,copy到堆:malloc,以便栈上的作用域结束后扔能访问到Block。所以一般用copy修饰,语义明确。
Block变量的捕获,__weak和__block
Block会捕获外部变量时有三种情况:auto——基础数据值拷贝,指针类型指针拷贝,会在数据结构中增加一个同名变量保存;__block——获取地址强持有变量,引用计数+1,封装成一个结构体byref,并增加assign和dispose函数维护引用计数;__weak——通过一个弱引用指针捕获对象,不强持有,一般用于打破block捕获变量造成的循环引用。__weak id weakObj;
forward指针的作用
block从栈上拷贝到堆上时,byref结构体中指向自身的forward指针会指向堆上的变量结构体对象。保证无论在栈上还是堆上都能通过byref->forward->obj访问到捕获的变量。
网友评论