NSTimer会保留其目标对象

来源 ——《Effective Objective-C 2.0 》Matt Galloway

计时器是一种很方便的对象。Foundation 框架中有个类叫做NSTimer,开发者可以指定绝对的日期和时间,以便到时执行任务,也可以指定执行任务的相对延迟时间。计时器还可以重复运行任务,有个与之相关联的”间隔值”(interval)可用来指定任务的触发平绿。比方说,可以每5秒轮询某个资源。
计时器要和”运行循环”(run loop)相关联,运行循环到时候回触发任务。创建NSTimer时,可以将其预先安排在当前的运行循环中,也可以先创建好,然后由开发者自己来调度。无论采用哪种方式,只有把计时器放在运行循环里,它才能正常触发任务。例如,下面这个方法可以创建计时器,并将其预先安排在当前运行循环中:

用此方法创建出来的计时器,会在指定的间隔时间后执行任务。也可以令其反复执行任务,直到开发者稍后将其手动关闭为止。target与selector参数表示计时器将在哪个对象上调用哪个方法。计时器会保留其目标对象,等到自身失效时再释放此对象。调用invalidate方法可令计时器失效;执行完相关任务之后,一次性的计时器也会失效。开发者若将计时器设置成重复执行模式,那么必须自己调用invalidate方法,才能令其停止。

由于计时器保留其目标对象,所以反复执行任务通常会导致应用程序出问题。也就是说,设置成重复执行模式的那个计时器,很容易引入保留环(retain cycle) 。要想知道其中的缘由,请看下列代码:

能看出问题么?如果创建了本类的实例,并调用其startPolling方法,那会如何呢?创建计时器的时候,由于目标对象是self,所以要保留此实例。然而,因为计时器是用实例变量存放的,所以实例也保留了计时器。于是就产生了保留环,如果此环能在某一时刻打破,那就不会出什么问题。然而要想打破保留环,只能改变事例变量或令计时器失效。所以说,要么调用stopPolling,要么令系统将此对象回收,只有这样才能打破计时器无效。除非使用该类的所有代码都在你的掌控之中,否则无法确保stopPolling一定会调用。而且即便能满足此条件,这种通过调用某方法来避免内存泄露的做法,也不是个好主意。另外,如果想在系统回收本类实例的过程中令计时器无效从而打破保留环,那又会陷入死结。因为在计时器对象尚且有效时,EOCClass实例的保留技术绝不会降为0,因此系统页绝不会将其回收。而现在又没人来调用invalidate方法,所以计时器将一直处于有效的状态。

当指向EOCClass实例的最后一个外部引用移走之后,该实例仍然会继续存活,因为计时器还保留着它。而计时器对象也不可能为系统所释放,因为实例中还有个强引用正在指向它。更糟糕的是:除了计时器之外,又没有别的引用再指向这个实例了,于是该实例就永远丢失了。内存泄露就产生了。这种内存泄露问题尤为严重,因为计时器还将继续反复地执行轮询任务。要是每次轮询都得联网下载数据的话,那么程序就会一直下载数据,这又更容易导致其他内存泄露问题。

单从计时器本身入手,很难解决这个问题。可以要求外界对象在释放最后一个指向本实例的引用之前,必须先调用stopPolling方法。然而这种情况无法通过代码检测出来,此外,假如该类随着某套公开的API对外发布其他开发者,那么无法保证他们一定会调用此方法。

这个问题可通过block来解决。虽然计时器当前并不直接支持block,但是可以用下面这段代码为其添加此功能:

好了,这个方法是怎样解决retain cycle的,大家马上明白。这段代码将计时器所应该执行的任务封装成block,在调用计时器函数时,把它作为userInfo传进去。该参数可用来存放不透明的值,只要计时器还有效,就会一直保留着它。传入参数时要通过copy方法将block拷贝到heap上,否则稍后要执行的时候,这个block可能已经无效了。计时器现在的target是NSTimer对象,这是个单例,因此计时器是否对保留他其实都无所谓。此处依然有保留环,然而因为类对象无须回收,所以不用担心。

这套方案本身并不能解决问题,但它提供了解决问题所需的工具。修改刚才那段有问题的范例代码,使用新分类中的eoc_scheduledTimerWithTimerInterval方法来创建计时器:

仔细看看代码,就会发现还是有保留环。因为block捕获了self变量,所以block要保留实例。而计时器又通过userInfo参数retain了block。最后,实例本身还要保留计时器。不过,只要改用weak引用,即可以打破retain cycle:

这段代码采用了一种有效的写法,它首先定义了一个弱引用,令其指向self,然后使block不捕获这个引用,而不直接区捕获普通的self变量。也就是说,self不会为计时器所保留。当block开始执行时,立刻生成strong引用,以保证实例在执行期间持续存活。
采用这种写法之后,如果外界指向EOCClass实例的最后一个引用将其释放,则该实例就可以为系统所回收了。回收过程中还会调用计时器的invalidate方法,这样的话计时器就不会再执行任务了。此处使用weak引用还能令程序更加安全,因为有时开发者可能在编写dealloc时忘了调用计时器的invalidate方法,从而导致计时器再次运行,若发生此类情况,则block理的weakSelf会变成nil。

Tips:
1.NSTimer对象会保留其目标,直到计时器本身失效为止,调用invalidate方法可令计时器失效,另外,一次性的计时器在触发完任务之后也会失效。
2.反复执行任务的计时器,很容易引入保留环,如果这种计时器的目标对象又保留了计时器本身,那肯定会导致保留环。
3.可以扩充NSTimer的功能,用blcok来打破retain cycle。不过,除非NSTimer将来在公共接口里提供此功能,否则必须创建分类,将相关的实现代码加入其中。



发表评论

电子邮件地址不会被公开。 必填项已用*标注