在咱们平常开发中,咱们或多或少的都会遇到循环引用的问题。其实问题的实质就是形成了互相持有的关系,在对象释放的时候,就好像产生了一个死锁同样,系统没有办法释放其中的任何一个对象,就形成了内存泄露的问题。咱们都知道NSTimer是其中的典型。但是为何继承自UIControl类的对象一样调用addtarget的方法就不会形成内存泄露的问题呢?如今就开启本文的探索。git
这是苹果作的一种设计模式,在设置target对象以后,该对象能够执行对应的Selector。咱们能够看到在咱们的项目中,常常在使用UIButton,UISegmentedControl等继承自UIControl的类时调用github
- (void)addTarget:(nullableid)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;这个方法,可是从代码可读性的角度考虑,这样的并非特别的好,咱们也常常为这些类写扩展,完成block的调用。可这种方式为何会存在,不是设计成block回调。其实这个缘由我的认为有两个。设计模式
1.在storyboard下,将selector链接出来就是使用的这一模式,这样的模式我的认为在这种状况下仍是很强大的。oop
2.其实这个模式是伴随整个OC的版本的,而block是在iOS4的时候才推出的。因此在开始的时候Target-Action的模式看起来真的很强大。并且我发如今iOS10中,苹果已经在NSTimer类中添加了block的方式,其实这时候咱们循环引用的问题能够用block的方式,但也只能在iOS10的时候使用。性能
其它关于此模式的思考再也不扩展,网上相关的文章不少,Google一下有不少,本文的核心在于去深刻的研究一小下。测试
上面是咱们调用的时候会调用的方法,可是UIButton不会形成循环引用,可是NSTimer为何会形成循环引用的问题呢?从这个问题出发,我查看了UIControl和NSTimer的官方文档,对于这里的解释真的是聊聊无几,我没有找到强有力的证据可以说明其中的缘由,可是咱们思考下猜测应该是UIControl机制下必定是底层将self弱引用了,解开了循环的链,因此UIControl下没有这样的操做。从这个角度出发,我去Google了一下,看了一些相关的文章,发现能够在堆栈信息中看出一些猫腻。那么如今看一下咱们堆栈信息中咱们可以发现什么.
优化
首先咱们看一下使用LLDB方案咱们获取到的信息是否是能够为咱们所用呢?我分别在两个addTarget方法出下了断点。而后在控制台输入dis,打印当前堆栈的调用信息,结果以下。设计
在看到这个堆栈信息的时候我发现对于同一块内存的引用方式居然彻底是同样的,这就更加增长了个人好奇,这里的堆栈信息彻底不能解答现有的疑问,还有其余的方式么?后来想到调用方法的堆栈,去看方法到底作了什么也许更清晰,咱们可以清晰地知道方法中用到了什么,因而在项目中添加了以下两个symbolic breakpoint断点践行进行测试。3d
此时从新跑程序,在每一个断点执行的时候,咱们能够看到对应的堆栈信息以下。代理
经过上图的两张堆栈信息,咱们能够看到在UIControl下的target的持有方式确实是weakRetained弱持有的方式解开了引用循环,因此咱们在使用时不会出现引用循环的问题。可是在NSTimer下,我看到的堆栈信息中看到这行代码的时候,开始明白机制的原理了,在NSTimer机制下对Target持有的方式使用的是autorelease的方式,也就是说target会在runloop下一次执行的时候查看这块区域是否进行释放,这也就能解释为何咱们若是将repeats属性设置成NO内存能够释放的缘由,以及为何将self设置成nil后内存依然不释放的缘由。接下来我对invalidate方法打印堆栈信息,可是我发现没有对应方法的堆栈信息,反而会再次调用addtarget方法,这是我联想到NSTimer的官方文档中有说明,一旦调用了invalidate方法以后,这个timer就不能再使用,我认为底层这个时候就是个当前的timer进行了一个target的重定向,正好执行一次runloop的timerobserver监听,将以前的内存释放掉了,而后解开了引用的循环,如今咱们已经明白了原理,那么咱们就从原理出发,看看现有的解决方案是否合理。
我百度了一下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方法,毕竟不是能看到的问题解决了,咱们的程序就没有问题了。
但愿本文能给你们在开发中带来帮助,最近一直都在作一些项目优化上的事,最近有时间会分享关于如何让程序变得更省电上的思考和一些优化上的小经验。若是文章中的观点有任何问题,烦请留言区指出,我会当即进行更正,谢谢。