美文网首页iOS-blockiOS点滴首页投稿(暂停使用,暂停投稿)
关于Block的使用和解决Cycle Retain问题(ARC)

关于Block的使用和解决Cycle Retain问题(ARC)

作者: Jack丶Tang | 来源:发表于2016-04-03 00:27 被阅读1018次

         本文只介绍了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这个方法,看看这个类本该释放�是否没有释放,�如果没有释放,再去研究并解决!这样积累的经验越多,相信看理论知识也能看得更深。

    相关文章

      网友评论

      • sonialiu:请问 arc环境下,strong修饰block属性和copy修饰block属性 是不是一样的?
        Jack丶Tang:不是自动copy到堆区,是在赋值的时候,编译阶段在block后面加一个copy。可是这个是属性,会走setter方法,setter方法是根据你定义的strong或者copy进行相应的retain和copy,你可以去看看属性的setter方法~
        sonialiu:@无敌小丸子 ARC环境下,系统不是自动执行copy吗,从栈区拷贝到堆区。也就是ARC情况下,block都是在堆区的,这样的话 使用strong 也是可以的呀??
        Jack丶Tang:@404你懂得 不一样的,用copy是为了将栈区的block copy到堆区,不是为了增加引用计数哦。如果用strong的话,arc只是对block进行了一次retain,这样对于栈区的block是没有作用的
      • a05832db24ea:请教一个问题,我是用swift开发,新手。之前有个回调功能首先想到自定义了delegate来实现,没问题,后来看到闭包,也就是block了,感觉实现我要的回调功能会更简单。于是就改写了。现在我也搞不清是否存在循环引用计数啊,使用场景很简单,就是类B中实现了一些功能,类A中定义了类B的实例,并且设置B.block={回调代码},就是这个回调代码里用到了self,也就是对类A做些操作处理,请问这样会造成循环引用吗?应该不会吧?
        a05832db24ea: @无敌小丸子 但是A类的一个成员变量指向的是一个B类的实例,该实例中的闭包又包含了A类的self,这样不是循环引用?必须闭包是属于A类实例才行?我觉得好像是间接循环引用一样。
        a05832db24ea: @无敌小丸子 谢谢,你这么一说我就明白了
        Jack丶Tang:@屎壳郎123 swift的闭包也是会造成循环引用的,造成的原因跟OC一样,你这种情况是不会的。因为B类的属性block闭包虽然持有了A类的self,这个A类的self对象并没有持有这个闭包,所以是不会造成循环引用的。
      • samingzhong:除了在dealloc打印判断之外,最好还是跑一下instrument-leak:smile:
        Jack丶Tang:@samingzhong嗯,是的,谢谢提醒,待会学习一下:joy:

      本文标题:关于Block的使用和解决Cycle Retain问题(ARC)

      本文链接:https://www.haomeiwen.com/subject/kjaylttx.html