印象笔记格式版 https://www.evernote.com/l/AG5VNoZAikZB9JURWgasfkp5GJAbnSknTdk

上周和两位同事讨论到block使用场景中哪种会发生retain cycle。一位是认为场景A会发生retain cycle,一位是认为场景A可能会发生retain cycle,最好采用全部weakSelf方式来编码,确保无遗漏不使之产生retai cycle。(场景A见下图)

A、基础知识点:

  1. Blocks are a C language extension for creating anonymous functions.

  2. The initial allocation is done on the stack, but the runtime provides a Block_copy function which, given a block pointer, either copies the underlying block object to the heap, setting its reference count to 1 and returning the new block pointer, or (if the block object is already on the heap) increases its reference count by 1. The paired function isBlock_release, which decreases the reference count by 1 and destroys the object if the count reaches zero and is on the heap.
  3. It does not provide a cycle collector; users must explicitly manage the lifetime of their objects, breaking cycles manually or with weak or unsafe references. 『MRC和ARC中都没有引用环回收器,所以ARC中,得更留意retain cycle发生场景』
  4. 根据Block在内存中的位置分为三种类型NSGlobalBlock,NSStackBlock, NSMallocBlock。
    • a、NSGlobalBlock:类似函数,位于text段(《obj-c高级编程》一书又说位于data段,但实质上两种都没关系);
    • b、NSStackBlock:位于栈内存,函数返回后Block将无效;
    • c、NSMallocBlock:位于堆内存(在heap内存区域,这是block和obj发生retain cycle的关键点)。以内存管理的理解方式,则相应说明三点,
      • a1、『NSGlobalBlock:retain、copy、release操作都无效;』;
      • b1、『NSStackBlock:retain、release操作无效。必须注意的是,NSStackBlock在函数返回后,Block内存将被回收。即使retain也没用。需要警惕的是,[[mutableAarry addObject:stackBlock],在函数出栈后,从mutableAarry中取到的stackBlock已经被回收,变成了野指针。正确的做法是先将stackBlock copy到堆上,成为NSMallocBlock类型对象再操作』;
      • c3、『NSMallocBlock:支持retain、release,也是本文重点关注的block类型』)。 Block对不同类型的变量(基本类型)的(截获)存取:a、局部自动变量,在Block中只读(当做常量使用)。b、static变量、全局变量。Block中可以对他进行读写。因为全局变量或静态变量在内存中的地址是固定的。c、Block变量,被__block修饰的变量称作Block变量,基本类型的Block变量等效于全局变量、或静态变量。
  5. BlockA被另一个BlockB使用时,另一个BlockB被copy到堆上时,被使用的BlockA也会被copy。但作为参数的BlockA是不会发生copy的。(作为参数传递的blk是不会被copy,也就是不会被强引用)
  6. MRC中__block是不会引起retain;但在ARC中__block则会引起retain。ARC中应该使用__weak或__unsafe_unretained弱引用。
  7. 一个常见错误使用是,开发者担心retain cycle错误的使用__weak。比如将Block作为参数传给dispatch_async时,系统会将Block拷贝到堆上(GCD把block当参数时,block会被copy到heap上成MalloBlock),如果Block中使用了实例变量,还将retain self,因为dispatch_async并不知道self会在什么时候被释放,为了确保系统调度执行Block中的任务时self没有被意外释放掉,dispatch_async必须自己retain一次self,任务完成后再release self。但这里(瞎担心retain cycle发生)故使用__weak,使dispatch_async没有增加self的引用计数,这使得在系统在调度执行Block之前,self可能已被销毁,但系统并不知道这个情况,导致Block被调度执行时self已经被释放导致crash。 * {试验结果:__weak 修饰的self在异步block回来后已经被释放了,所以确实是无法执行block中self相关操作。但是此时self的地址已经被设置nil,不会造成crash。造成crash的情况,有可能是MRC下用__block修饰,或者其它复杂情况下的ARC。试验中,一个特别现象是,如果blockA最后是self的强引用,如果此时GCD切换入一个新的blockB,那将直接接触对self的强引用,那么GCD_blockB回来后,后面如果有self相关调用将是无效的。}
  8. 可以用dealloc方法来管理一些资源,但不能用来释放实例变量,也不能在dealloc方法里面去掉[super dealloc]方法,在ARC下父类的dealloc同样由编译器来自动完成。(debug方式:可以使用dealloc来检测obj是否在预期中被释放,用chisel在极端情况下检测确切内存地址中是否还存在obj)

B、关键知识点:

  1. 核心点是应指明,对象间是哪种引用类型。是强引用,发生了retain count 加1;还是弱引用,未对retain count做操作。
  2. 两个obj间的retain cycle很容易看出来,(强引用符号===>,弱引用符号- - - >)。就像: ===>objA(self)===>blkB===>objA(self) 三个或多个obj(block)间的retain cycle最容易出现遗漏,三个就像: ===>objA(self)===>objB===>blockC===>objA(self);五个就像:===>objA(self)===>objB===>blockC===>objD===>blockE===>objA(self);
  3. MRC中__block是不会引起retain;但在ARC中__block则会引起retain。ARC中应该使用__weak或__unsafe_unretained弱引用。
  4. block的类型,取决于是否截获(快照)自动变量:globalBlock不依赖于执行时的的状态,所以整个程序中只需一个实例。arc情况下一般都是stackBlock类型便于执行完立即回收,如果有特别需要,可以将stackBlock拷贝到heap上转成mallocBlock再进行使用。(《obj-c高级编程》一书在page 111~112,)
  5. block传递时相应产生的类型:
    • a、作为参数的Block是不会发生copy的。
    • b、将block作为函数返回值时,编译器会自动生成复制到堆上的代码(即转成mallocBlock)。(这里可以看出设计原则为:Block在未来需要使用时将放入heap,只需用一次的放到stack中) mallocBlock常见情况和需要转成mallocBlock情况:stackBlock时,以下方法或函数不用手动复制(即转成mallocBlock),a、cocoa框架方法中函数usingBlock的;b、GCD 的api ,将block做参数(mallocBlock)。需要手动复制成mallocBlock场景:『相反的,在NSArray类的initWithObjects实例方法上传递Block时需要手动复制』《objc-c高级编程》

psNote: 以上2、3点中红色的===>,就是引起retain cycle的强引用,应该改成 - - - > 弱引用,使用__weak符号修饰。 强引用符号===>,弱引用符号- - - >,无引用 ~ ~ ~>

场景试验

以下是几种场景的检验。


试验一:场景A,静态方法传参block (去除图中@weak标识符)

引用流程是:

===>self~ ~ ~>static_method~ ~ ~>blk_callback(stackBlock)[http返回后,以blk_callback(mallocBlock)回调]<==>blk_callback(mallocBlock)===>self

试验一两个关键点,self不会在第一步强引用blk_callback(stackBlock);潜在的闭环不成立。 回调的block_callbank(stackBlock转成mallocBlock) ,会对self强引用一次,等待block执行完后方式对self的强引用。

这里合适的处理策略,应该让block_callback 强引用self,避免block_callback回调时,self成了野指针或nil值。


试验二:场景B,典型的三元retain cycle (去除图中__weak标识符)

引用流程是:

===>self===>vc_obj===>vc_tapBlock===>self

这是典型的三元引用环场景。应该使用 __weak修饰符后的 weakSelf,将vc_tapBlock对self的引用设置为弱引用。


试验三:场景C,还是典型的三元retain cycle (去除图中__weak标识符)

引用流程:

===>self===>_mcs_notiObj===>bulk_notiCallback===> self

同上,试验二。


试验四:场景D,GCD函数传参block

引用流程:

===>self~~~>GCD_blkPara(mallocBlock)===>self

self不存在持有dispatch_async的block参数的可能,因此需要block_gcd_para强引用self,当block回调时,保证self还未释放。这个用法正确,相反若使用weakSelf则是不对的。


试验五:cocoa中基础obj使用usingBlock

引用流程:

===>self===>arrObj===>block_usingBlock===>self

由于self对arrObj的强引用是初始引用,无法weak操作,所以只能(必须)在最后一步将block_usingBlock对self的强引用设置成weakSelf;


试验六:业务多重嵌套,block和GCD深度强引用

从试验一到试验五,我们其实可以归纳出引用环retain cycle的关键点,明确第一步是哪种引用(strong还是weak),如果是weak那么肯定不存在retain cycle放心使用;如果是strong引用,那么最后一步是否使用到self,如果使用必须采用weakSelf方式;

所以,再复杂的实际嵌套业务也不会出现模糊无法判断的情况。

简要总结

关键是检查第一步:确认self是否强引用了block,最容易出现的情况是持有实例obj中自定义block; 检查最后一步block是否强引用self:如果第一步是强引用;

Result Note :

  1. 场景A,是不会产生retain cycle;场景B是典型的三角引用环;
  2. 全部weakSelf的方式,看似多一步确保不发生retain cycle,但是违背了Cocoa(MRC、ARC)对block内存管理的初衷,严重的一方面会产生致命crash(如上A-8),或者无法执行相应任务块。不理性地一方面就像担心程序处处bug,所以处处都@try exception,增加了性能负担;
  3. 《obj-c高级编程》并没有推荐将stackBlock转成mallocBlock再使用。而是在特殊需求下,可以在stackBlock特点不能满足时,将其转成mallocBlock再使用,说明了转化的方式和技巧。
  4. 《obj-c高级编程》书重点点名,两种Block是不应转成mallocBlock的(本身就是,而且不应该去管理到这区域的block)。如上B-7点的,这两种为a、cocoa框架方法中函数usingBlock的;b、GCD 的api ,将block做参数(mallocBlock)。

参考资料 :

  1. http://clang.llvm.org/docs/AutomaticReferenceCounting.html 2.《objective-c高级编程 iOS和Mac OX多线程和内存管理》
  2. http://tanqisen.github.io/blog/2013/04/19/gcd-block-cycle-retain/

工具使用:

  • 对象地址检测工具:LLDB 的facebook增强版 chisel,Chisel is a collection of LLDB commands to assist in the debugging of iOS apps.https://github.com/facebook/chisel

概念:

**retain cycle: **retain cycle,即『强引用环』,表现为两个或多个obj(blk)相互强引用导致相互无法释放,最后成了内存中的孤岛,是内存泄露的一种典型情况。实质的逻辑,就类似于线程死锁,数据互持。

一般情况只有经典的古文才能让我感觉到『澎湃』的感觉。偶然也有一些让人回味的现代文。所以搭了个deep-paper.com的来收藏某些经典的文章。

IT工程师对感情和生活的思考确实比重太少了。自我检讨一下,工作事业本身终究是回向给家人朋友,愿和他们一起简单快乐的生活。而学会愉快地和人交流相处才是人生意义所在。

近来从两篇文章得到两个很有里程碑意义的启发。

第一个来源:《为什么你的爱情会输给日常琐碎?》

核心点是,『感情中,伴侣处理对方情绪需求的方式最为关键。当伴侣发出和对方“连接”的情绪需求时,面对请求的不同回应方式,会对夫妻的幸福程度造成深远的影响。』

回想我当年大学那个长跑的初恋,演化从愉快、厌恶、磨合、反复磨合、再反复磨合、舒适、自然、眷恋、交流闭环愉快、谈婚论嫁、交流情趣变味、突然急刹车。

长跑中,待对方我是问心无愧了,从牺牲了我的狗肉朋友圈,到勒紧裤腰带满足对方各种无用小东西的爱好,再到磨钝性格棱角主动妥协融入对方的世界观和坐标系。到后来已成习惯了一个亲人的存在。

但是,一切终止在个地方。创业落魄时,我说我好伤心,她竟然好像确实没有听到,而我就伤心地过界了,潜意识感觉已经到缘散不留之时。反而却有了『轻松』的感觉。这可能就是情绪需求是双方关系最大的影响因素了吧。

第二个来源:《有情趣的生活,不一定非要价格昂贵》

核心点是,『生活是有仪式感的,我们想要尊重生命给予的一切,多去感受其中有趣的体验。正是我们看重的仪式感让生活称其为生活,而非简单快速的生存方式。』

认真的收拾屋子、认真的整理物件、认真的准备早餐。。。认真考虑生活中更实际有趣的存在习惯。这些是很实在的大事,舍近求远地憧憬远方并非智慧的生活行为。远方可以有,不过近处总有不少可以看清晰的小事。

今天是上元节元宵,补睡起来到了21:30,:sleeping: 看看wm家里的摄像头诺诺在咿咿呀呀地跳舞,又看看电视晚会上有啥新鲜事,再看看月下楼随机一篇古诗词。不能够陪在家人旁边,过一个个简单而真实的日子,是咱这一辈年轻人的遗憾了,也常在这种情景时刻倍感心慌慌,,:smile:

静下心来,检视一下自己,当遇到心慌慌的时候,除了增强跑步强度加大多巴胺地强壮感愉悦感之外。最可消除烦闷忧愁的,还有两个有趣的方式:

元宵诺诺

  1. 入境诗词文字: 想象在曾经的某片时空景象,古人偶感得神来之笔,其意气之精妙、精神之独到,让人得到的感动绝非千金可求来啊!!! :pray:
  2. 透视各种澎湃的『基础建筑』: 比如,阅读、推演、审视那犹如浩大交响乐,跌宕起伏的古代人类文明历史;再比如,hack推测各种程序或模式的liveObjMap在内存中的大概存在形态。

第一点,需要那么一点耐心,一点平静中的偶然,才可能捕获到下面一些隽永的共鸣。第二点就得切换各种视角啥的。这里表现第一点,也当是汇总最近让我印象深刻的句子们,



1
2
3
4
5
6
7
8
9
君子对青天而惧,闻雷霆而不惊;履平地而恐,涉风波不疑  ---《小窗幽记》陈继儒(明) ---	
心无机事,案有好书,饱食晏眠,时清体健,此是上界真人。 ---《小窗幽记》陈继儒(明) ---
常将酒钥开眉锁,莫把心机织鬓丝 ---《增广贤文》之二百八十 ---
费尽心机俱归无用,总不如安分随缘 ---
处世若大梦,胡为劳其生?...浩歌待明月,曲尽已忘情。---《春日醉起言志》李白 ---
克己成,纵己败 ---
书屋前,列曲槛栽花,凿方池浸月,引活水养鱼;
小窗下,焚清香读书,设净几鼓琴,卷疏帘看鹤,登高楼饮酒。小酌半醺,浇花种竹;听琴玩鹤,焚香煮茶;泛舟观山,寓意奕棋。虽有他乐,吾不易矣。---《小窗幽记》 ---
掩户焚香,清福已具。如无福者,定生他想。更有福者,辅以读书。---《小窗幽记》 ---

tip: 《小窗幽记》,《菜根谭》、《围炉夜话》这三本典雅闲适心境的奇书,是浇灭心慌慌的无明心火的最佳选择了。