iOS Target-Action模式下内存泄露问题深入探究

在我们日常开发中,我们或多或少的都会遇到循环引用的问题。其实问题的实质就是造成了互相持有的关系,在对象释放的时候,就好像产生了一个死锁一样,系统没有办法释放其中的任何一个对象,就造成了内存泄露的问题。我们都知道NSTimer是其中的典型。可是为什么继承自UIControl类的对象同样调用addtarget的方法就不会造成内存泄露的问题呢?现在就开启本文的探索。

1.Target-Action模式

这是苹果做的一种设计模式,在设置target对象之后,该对象可以执行对应的Selector。我们可以看到在我们的项目中,经常在使用UIButton,UISegmentedControl等继承自UIControl的类时调用

- (void)addTarget:(nullableid)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;这个方法,但是从代码可读性的角度考虑,这样的并不是特别的好,我们也经常为这些类写扩展,完成block的调用。可这种方式为什么会存在,不是设计成block回调。其实这个原因个人认为有两个。

1.在storyboard下,将selector连接出来就是使用的这一模式,这样的模式个人认为在这种情况下还是很强大的。

2.其实这个模式是伴随整个OC的版本的,而block是在iOS4的时候才推出的。所以在开始的时候Target-Action的模式看起来真的很强大。而且我发现在iOS10中,苹果已经在NSTimer类中添加了block的方式,其实这时候我们循环引用的问题可以用block的方式,但也只能在iOS10的时候使用。

其它关于此模式的思考不再扩展,网上相关的文章很多,Google一下有很多,本文的核心在于去深入的研究一小下。

2.UIControl和NSTimer下调用addTarget方法到底为什么不同

Target-Action模式.

上面是我们调用的时候会调用的方法,但是UIButton不会造成循环引用,但是NSTimer为什么会造成循环引用的问题呢?从这个问题出发,我查看了UIControl和NSTimer的官方文档,对于这里的解释真的是聊聊无几,我没有找到强有力的证据能够说明其中的原因,但是我们思考下猜想应该是UIControl机制下一定是底层将self弱引用了,解开了循环的链,所以UIControl下没有这样的操作。从这个角度出发,我去Google了一下,看了一些相关的文章,发现可以在堆栈信息中看出一些猫腻。那么现在看一下我们堆栈信息中我们能够发现什么.

首先我们看一下使用LLDB方案我们获取到的信息是不是可以为我们所用呢?我分别在两个addTarget方法出下了断点。然后在控制台输入dis,打印当前堆栈的调用信息,结果如下。

在看到这个堆栈信息的时候我发现对于同一块内存的引用方式竟然完全是一样的,这就更加增加了我的好奇,这里的堆栈信息完全不能解答现有的疑问,还有其他的方式么?后来想到调用方法的堆栈,去看方法到底做了什么也许更清晰,我们能够清晰地知道方法中用到了什么,于是在项目中添加了如下两个symbolic breakpoint断点践行进行测试。

symbolic breakpoints

此时重新跑程序,在每个断点执行的时候,我们可以看到对应的堆栈信息如下。

UIControl 下的target
NSTimer下的target

通过上图的两张堆栈信息,我们可以看到在UIControl下的target的持有方式确实是weakRetained弱持有的方式解开了引用循环,所以我们在使用时不会出现引用循环的问题。但是在NSTimer下,我看到的堆栈信息中看到这行代码的时候,开始明白机制的原理了,在NSTimer机制下对Target持有的方式使用的是autorelease的方式,也就是说target会在runloop下一次执行的时候查看这块区域是否进行释放,这也就能解释为什么我们如果将repeats属性设置成NO内存可以释放的原因,以及为什么将self设置成nil后内存依然不释放的原因。接下来我对invalidate方法打印堆栈信息,但是我发现没有对应方法的堆栈信息,反而会再次调用addtarget方法,这是我联想到NSTimer的官方文档中有说明,一旦调用了invalidate方法之后,这个timer就不能再使用,我认为底层这个时候就是个当前的timer进行了一个target的重定向,正好执行一次runloop的timerobserver监听,将之前的内存释放掉了,然后解开了引用的循环,现在我们已经明白了原理,那么我们就从原理出发,看看现有的解决方案是否合理。

3.从根源出发,看看现有解决方案

我百度了一下NSTimer循环引用的问题,归纳总结一下,大概的解决方案是

1)及时的调用invalidate方法 

2)给NSTimer写一个扩展类,然后使用block回调的方式

3)在给self增加代理的时候创建中间层代理。

那么我们现在看到三个方法的时候,首先知道方法一重定向的方式在上边已经知晓了能够解决问题的原因,那么我们看下方法2和方法3是不是能够解决问题。

首先方法二实现的核心代码大致如下

看完上边的代码,我们发现此时的target为NSTimer类对象,其实本身就是一个单例,所以会伴随程序的整个生命周期,所以程序是不是保留对他的循环引用都已经无所谓,所以不会造成内存泄露的问题,但是我们需要思考的一件事,我们的程序还是依然会在我们看不到的地方不停地去执行repeats事件,如果我们程序中有很多的NSTimer这样的事件用这样的方法,因为不太了解底层的具体实现,但是我认为这样的方案对于程序的性能上会有一定的影响。但是对于内存释放上的考量我认为问题已经得到了解决。所以我的建议是即便用这样的方案也要及时的调用invalidate方法,否则程序的性能会受到影响,当然我们的项目也用到了很多这样的方法,因为我认为在代码可读性的角度出发,所以这样使用时不要觉得内存问题解决了就完事了。

看完了方法2中的问题,我们现在再来看方法3是如何解开循环引用的。我在github上下载了一个相关demo,核心源码大致如下。

我们看到作者重新写了一个类,使用这个类老作为target,解开了循环引用,这个时候测试delloc方法就不会出现循环引用,看似创建timer类的解决了循环引用的问题。但是我测试验证了我的想法,作者创建的weakTimer对象就会常驻内存一直都无法释放掉的。其实如果作者在中间层将target指向一个类对象,我认为这样的方法还是能够解决很多问题的,但是关键还是在于上边所说,还是可能会引发性能问题,而且还需要在写对应的invalidate方法等,我觉得这个时候其实这样的方法本身意义就已经不大了。所以对于中间代理的方式,个人认为真的可用性不大,增加了程序的复杂度,还不能本质上的解决问题。

所以最后对NSTimer的使用个人建议就是创建扩展,我认为这样的方式代码的可读性是最强的。但是注意和平时使用时一样及时的调用invalidate方法,毕竟不是能看到的问题解决了,我们的程序就没有问题了。

希望本文能给大家在开发中带来帮助,最近一直都在做一些项目优化上的事,最近有时间会分享关于如何让程序变得更省电上的思考和一些优化上的小经验。如果文章中的观点有任何问题,烦请留言区指出,我会立即进行更正,谢谢。

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

推荐阅读更多精彩内容

  • __block和__weak修饰符的区别其实是挺明显的:1.__block不管是ARC还是MRC模式下都可以使用,...
    LZM轮回阅读 3,278评论 0 6
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,121评论 29 470
  • 多线程、特别是NSOperation 和 GCD 的内部原理。运行时机制的原理和运用场景。SDWebImage的原...
    LZM轮回阅读 2,001评论 0 12
  • iOS面试小贴士 ———————————————回答好下面的足够了------------------------...
    不言不爱阅读 1,960评论 0 7
  • 史上最全的iOS面试题及答案 iOS面试小贴士———————————————回答好下面的足够了----------...
    Style_伟阅读 2,344评论 0 35