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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • 1.1 局部变量 局部自动变量,在Block中可被读取。Block定义时copy变量的值,在Block中作为常量使...
    陈雨尘阅读 2,920评论 4 31
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,132评论 30 470
  • __block和__weak修饰符的区别其实是挺明显的:1.__block不管是ARC还是MRC模式下都可以使用,...
    LZM轮回阅读 3,293评论 0 6
  • 1. Object-c的类可以多重继承么?可以实现多个接口么?Category是什么?重写一个类的方式用继承好还是...
    Charls丶阅读 746评论 0 0
  • 虽自己不曾写过情书,但对古人写情的诗句甚是喜欢,几乎每一句都是一段缠绵的情,分享给有缘人,望其能贴合今日心境,聊以...
    Coral阅读 2,505评论 4 8