想来作为iOS开发者,一定对于block不那么陌生,block是苹果对于C的一个拓展,是匿名函数.我们通过对代码块的封装可以作为方法参数实现网络异步请求,也可以作为方法返回值进行链式编程,使用block代替代理等等...
那么这么神奇的一个Block,在它身上到底会发生什么事呢?先让我们简单的认识它:
一.Block的简单使用:
1.使用block进行简单的传值:(异步回传)
这里是登录一个请求,我在ViewModel里面做了请求,需要把返回的登录数据返回VC,告知它我这里登录成功,那么我们就可以采用这种Block进行回传.(这里只是为了演示,实际开发不建议这么做,在MVVM中,最恰当的方式是监听VM的属性再进行页面操作)
接下来的事情就很简单了,我们对Block进行赋值,来获取数据.(这里的dataReturn在VM初始化就进行赋值)
2.将Block作为方法返回值进行链式编程
比如我在Label分类中创建一个StyleConfig类提供项目Label的大体样式,再在分类中通过runtime引出这个属性,那么我就可以通过block枚举传参的形式来迅速设置label的样式.
我们在分类中来调用分类的样式,通过Block的参数枚举来判断该使用哪个样式
让我们来看看如何使用Block来链式设置UI
二.Block的循环引用问题:
其实关于这个问题,网上已经有很多文档去说明了,这里还是简单介绍下解决Block循环引用的几种方法:
1.引起循环引用的原因:
VC--强持有-->block--强持有-->VC
这里值得一提的是此时这个block不管是否被调用,都会造成内存无法释放.因为block此时已经捕获了外界的__strong对象.
2.循环引用解决方案
a.使用__weak __strong的组合技来解决,简单说一下 这里的__strong再次强引用是为了防止vc的提前释放而导致想要使用的vc为空.因为此时强引用的vc在block内部的生命周期已经被block管理,只在block内部的作用域之内,所以此时出了block,这里的vc还是会被正常释放,不会造成内存泄漏.
b.使用__block来解决循环引用
值得一提的是,如果要采用__block来解决循环引用问题的话,这里需要在block里面当不需要使用vc的引用属性的时候要在生命作用域里手动将其置空,并至少保证要将其调用一次.
为什么__block能解决这里的循环引用问题呢?容我卖一个关子,等我后续解答.
c.将vc作为参数传入block
前面有提过block也是匿名函数,如果将vc作为形参传入block中 那么其vc指针的生命周期就只在block内部,当block执行完毕其指针就会被释放,也就是说,其引用次数也会被-1,block持有vc的引用链就会被断掉,也就不存在循环引用问题了.
三.Block的三种类型:
在OC中,block有三种类型:
__NSStackBlock(栈block)
__NSGlobalBlock(全局block)
__NSMallocBlock(堆block)
1.__NSStackBlock:
栈Block在MRC中,如果block有捕获到外界变量,此时的block为栈block.
2.__NSMallocBlock:
如果这个栈区的block被copy,此时这个block就是MallocBlock(堆block)
值得注意的是如果在ARC中,此时只要捕获外界变量,那么就直接是MallocBlock,因为ARC下会自动将这个栈block copy到堆中.
3.__NSGlobalBlock:
如果记述全局变量的地方有声明block,且这个block不使用捕获的外界变量或者根本没有捕获外界变量,此时的block为__NSGlobalBlock.
四.Block的底层剖析:
我们先在.m文件中写一个block:
我们将oc的.m文件用clang -rewrite-objc 命令将其转成c++语言,来揭开它的神秘面纱:
搜索__block_impl 可以看到这里:
可以看到block其实也是个结构体!
注意这个void *isa!!!!!! 这个isa说明它实质上也是个对象!
我们再次搜索这个结构体找到对其赋值的地方看下他们依次的作用
1.isa:
可以发现这个结构体其实存储的是block的信息,它的isa指向了_NSConcreteStackBlock,说明它是个栈block.
2.funcPtr:
funcPtr指向我们执行这个block所调用的函数实现.
3.flags:
这个flags就是标记这个block此时的状态,因为C语言是静态语言,无法获取block此时的状态,那么就难以去管理其的生命周期,这里的flag就是标记其此时的状态.这里的flag其实不止是对其生命周期有管理,也有存储了里面的函数签名,是否全局等信息
4.desc:
而desc你可以发现,实际是个结构体_block_desc,这个结构体就存储了block的描述信息:
值得一提的是它的copy和dispose函数指针,这两个函数指针用来对block内部变量进行retain和relese操作,管理它们的生命周期.当block从栈copy到堆中时,其内部的变量以及捕获的外界变量将调用这个copy函数对其进行retain操作进行持有,而当block跳出其所在的作用域不再被外界持有时候也即是当其需要被释放之时,将调用dispose函数进行将内部的变量和捕获变量进行relese.
block如何进行捕获外界变量?
我们可以发现原代码__block int a = 0;在.cpp文件中已经被变成__block_byref_a_0 *a;
我们点进去查看:
这说明当我们用__block标注外界变量时候,block将其转为了一个结构体,并用指针指向它进行持有.
内部同样是4个参数:
1.__isa:
看到isa,我们就知道其实现在这个a也是个对象,这里的isa就是用来包装这个结构体的.通过这个isa可以区分它的具体类型.
2.int a:
这里存储的就是捕获的外界a的值.
3.__forwarding:
可以发现这个__forwarding指针指向的结构体类型与__block a的类型一致,实质上这个forwarding也是指向的自身.这个__forwarding其实是block安全取值的指针.
我们来看下它如何取值:
原代码:
printf("%d",a);
替换后:
可以发现它是通过a->__forwarding->a来进行访问其捕获的外界变量
中间为何要通过__forwarding指针来取值呢?
其实因为a是个局部变量保存在栈区,而block是结构体在堆区,试想如果a在方法执行完毕后那么原值会被系统释放,此时如果block被调用如果是直接a->a(即是访问上图中结构体里的int a,这个int a 就是保存的外界的栈中的a),那么a能取到值吗?肯定不可以的.
__forwarding指针的作用就是标记当block被copy到堆中,在捕获的外界变量被__block标记后,__forwarding指针就会将其原本指向栈中变量的地址转为指向堆中的a结构体,此时里面的a就会转为堆里的a.所以通过a->__forwarding->a去取值是能够保证正常取到a的.
这也就是为什么__block标记后就能修改原a,而不进行这样标记就无法修改值,因为c语言里的a是基本变量,基本变量的传值是值传递.而被__block进行捕获后已经转为了对象,拷贝进堆区后传到block内的其实是在堆区的a地址,这里的传值是地址传递!(鉴于篇幅有限,就不再实验演示了,有兴趣的可以自己打印下地址进行验证)
4.__size:
这里的size顾名思义其实就是标记当前结构体所占大小(字节数).
5.__flags:
这里的__flags是一个标志位变量.
这里再留个悬念,大家思考下如果变量没有被__block标注,在block中又如何展现呢?
(其实答案应该显而易见吧?haha)