本文只介绍了ARC时的情况,有些细节不适用于MRC。比如MRC下__block不会增加引用计数,但ARC会,ARC下必须用__weak指明不增加引用计数;ARC下block内存分配机制也与MRC不一样(ARC下会将栈区的Block在赋值的时候copy到堆区,从而导致截取的堆区变量引用计数增加),所以文中的一些例子在MRC下测试结果可能与文中描述的不一样
简介:这是一篇讲解如何使用Block,以及在使用过程中如何避免Cycle Retain的文章。如果想要知道Block的深层次的实现,可以去看<Objective-C 高级编程 iOS与OS X多线程和内存管理>的Block篇,书中详解了Block的底层实现。
一、Blcok的�优点和种类
1、Block的优点
Block虽然会由于使用不当,而导致Cycle Retain,但还是有很多优点的。语法简洁,回调方便,思路清晰,还有就是Block作为C语言的扩展执行效率较高。这样用文字说明可能不�直观,直接上代码做对比。通知的设计模式是开发过程中常用的,以使用Block回调和不使用Block的方式来作对比。
图一:对比通过对比,使用Block的接收通知处理和通知接收的方法紧密的黏在一起,直观明了,不过这里有个大坑,待会会提到。是否感受到Block的好处了呢,如果是,那么以后就多用吧,它会让你的代码思路更连贯!
2、Block的种类
Block不就是匿名函数么,还有种类?这个种类不是说形式上的种类,而是根据Block在内存中存储区域的不同而分的种类,有三种:Stack(栈区),Malloc(堆区),Global(全局)。之所以要在这里提到这三种Block,是因为后面的Cycle Retain就是由于Malloc(堆区)的Block导致的。在OC中堆区的内存管理都是用引用计数来管理的,而Stack和Global都是没有引用计数的,当它们超出作用域后,就会失去作用。那么Stack(栈区),Malloc(堆区),Global(全局)的block怎么判断,它们分别有哪些呢。
(1)判断方式
图二:判断Block的内存区域在代码中,我们定义了一个全局静态区的变量,通过它和block地址的对比,可以发现它们差不多,也就是说这个Block是Global(全局)的。同样的方式,Stack(栈区),Malloc(堆区),都可以判断出来。如果你觉得这种判断方式太low的话,Clang可以查看中间代码(C++),打开终端用Clang -rewrite-objc 编译你的文件,就可以看到中间代码了。说了不说原理的,不然太长了。如果想用这种方式判断的话,可以去看看这篇博客:iOS中block实现的探究。
(2)Stack(栈区),Malloc(堆区),Global(全局)的Block有哪些
以下所说的都是在ARC模式下
图三:各个种类的Block二、Block的使用
之所以写这一部分,是因为一些初学者,连基本的Block都不会使用,也不知道用在什么情形下,下面就是说Block用在什么情况下,又怎么用,如果你已经会用了,可以跳过这一部分。
1、用于两个类之间的通信
这是开发中最常用的,也就是ViewController和View,ViewController和ViewController之间的通信,这个通信就包括传值或者让另一个对象执行一些处理。这个思路和delegate(代理)很像,不过Block更简洁。这里就不上代码了,因为代码实在是不好上啊!如果真的需要的可以私聊我。
2、用于�方法的回调
这种使用情况,也是常用的,系统和很多第三方都用了这样的方法。还是以前面接收通知的Block为例子
图四:通知中心用的Block我们来分析一下这个方法的最后一个参数usingBlock,跟前面一样,在:后面都是跟的参数类型,那么usingBlock后面也是跟的参数类型,那么这个参数类型就是没有返回值、参数为note(NSNotification类的对象)的Block类型(后面的block为参数名)。那么接下来,我们就自己定义一个类似的方法,让它有回调Block
图五:回调Block这样,我们就定义了一个没有返回值,没有参数的Block类型,这个类型的变量为block,并且在函数内部实现回调,这样,我们就实现了和前面系统通知所写的一样的Block回调。当然在写Block类型的时候,是不会这样写的,而是用typedef。
这就是Block的两种常用用法,当然这是最基本的。下面就进入本文的重点,如何避免在使用Block的过程中造成的Cycle Retain。
三、避免Cycle Retain
1、Cycle Retain
retain cycle问题的根源在于Block和obj可能会互相强引用,Malloc(堆区)Block的内存管理方式也是引用计数,它的内部实现和类一样,都是通过isa指针指向堆区的该类型对象,可以说Malloc(堆区)Block就是一个类的对象,而被block截取的变量,就作为它的"属性",会被retain一次或者copy到堆区(如果它是在栈区的话)),互相retain对方。比如A和B两个对象,A持有B,B同时也持有A,按照上面的规则,A只有B释放之后才有可能释放,同样B只有A释放后才可能释放,当双方都在等待对方释放的时候, retain cycle就形成了,结果是,两个对象都永远不会被释放,最终内存泄露。
图六:相互持有(Cycle Retain)根据这个原理,那么会造成Cycle Retain的情况就只有三种。
一种是:block作为某个类的属性,可是它又截取了这个类的对象,从而导致Block retain了一次这个对象,这个对象又retain了一次这个Block(作为属性的时候会用copy,引用计数加一)。以ViewController这个类为例
图七:block作为属性我们发现这种情况,xcode会给我们警告,所以这种情况是很容易发现并解决的,用__weak typeof(self) weakself = self;来代替block里面的self,就可以了。
第二种:这种情况很难发现,但是很好解决(解决方法一样)。那是什么呢,其实本质还是一样,就是一个类的对象retain或者copy了这个Block,而这个Block又同时持有了这个类的对象,导致互相不能释放,因为block不能释放,导致其它被这个block截取的对象也无法释放。还是以通知为例(请原谅我,我真的超级喜欢用通知~)
图七:对象被没有释放的block持有这段代码的思路是,当我接收到通知的时候,我就改变ViewController的颜色,然后在当ViewController释放的时候移除通知。可是这会导致Cycle Retain,导致ViewController不能释放。解决办法你可能也知道,跟上面一样,block里面放weakself。可是为什么呢?这个Block我们没有作为属性,ViewController并没有retain它,只是Block retain了ViewController而已,没有造成Cycle Retain。我们先看一段官方文档:
图八:通知参数block的官方解释翻译一下:这个block会再接收到通知的时候执行,这个block被通知中心copy并且直到观察者被移除的时候才会移除。也就是说这个block会一直被通知中心持有,直到观察者被移除,它才会被释放。很好,问题解决了。block一直被通知中心持有,而block又retain了一次ViewController,导致ViewController不能释放(引用计数不能为0),这样ViewController就不会走dealloc这个方法。解决办法也是一样:
图八:解决办法第三种:这种情况和第二种情况原理一样,但是是最常遇到的,所以单独拿出来讲。这种情况是在项目中,用MJRefresh这个第三方的时候发现的。其实,只要懂了Cycle Retain的问题根源,这种情况也是很好理解的。
tableView.mj_footer = [MJRefreshFooter footerWithRefreshingBlock:^(void)refreshingBlock]
当tableView进行上拉加载的时候,会触发这个这个回调refreshingBlock,执行相应的加载操作(跟新数据),如果在refreshingBlock里面用了self,也会导致Cycle Retain,那这又是为什么呢。把这个方法点进去之后可以看到它的实现:
图九:方法的内部实现可以看到,方法的实现中,把block作为属性�赋值给MJRefreshFooter对象并且返回作为tableView的属性。我们知道所有的View都被ViewController retain了一次(view的生存周期),如果block作为view的属性,那就相当于self.view.tableView.mj_footer.refreshingBlock;所以refreshingBlock前面所有的对象:self、tableView、mj_footer都不能被refreshingBlock retain,如果有一个被retain了,那就是Cycle Retain!�这里我们仍然用__weak指针打破Cycle Retain。解决方法一样,这里就不详解了。
2、��不能滥用__weak指针
__weak指针可以解决Cycle Retain问题,但是不能乱用比如gcd和UIView的Animation等等,因为Block没有retain那个对象,虽然不会像MRC下那样造成Crash,但是还是可能会导致没法实现你要的功能。例子如下:
图十:乱用__weak指针这里我们让dispatch_async中的队列延迟5秒执行,�在执行队列前按下button,让self释放掉(dissmiss),这样self会为nil,可是我想要在5秒后让它输出"test",由于self已经被释放变为nil,虽然不会crash或者内存泄露,但是我想要实现的功能却不能实现了。
将Block作为参数传给dispatch_async时,系统会将Block拷贝到堆上,如果Block中使用了实例变量,还将retain self,因为dispatch_async并不知道self会在什么时候被释放,为了确保系统调度执行Block中的任务时self没有被意外释放掉,dispatch_async必须自己retain一次self,任务完成后再release self。但这里使用__weak,使dispatch_async没有增加self的引用计数,这使得在系统在调度执行Block之前,self可能已被销毁,但系统并不知道这个情况,可能导致有些功能不能实现。
总结:要想用好Block就得多写、多用,当Block作为属性的时候,就值得你去关注Retain Cycel的问题了。
最后也是最重要的,如果有用到Block,�尽量在那个类里写下-(void)dealloc这个方法,看看这个类本该释放�是否没有释放,�如果没有释放,再去研究并解决!这样积累的经验越多,相信看理论知识也能看得更深。
网友评论