【iOS面试题】2.Timer的循环引用问题

814 阅读4分钟

在循环引用的问题中,面试官最喜欢问到的就是Block和Timer的循环引用问题,今天就Timer的循环引用问题,进行分析。

一般问题的场景

需要用到Timer的场景一般是UI页面需要做定时的操作,比如定时翻动轮播图,或者定时刷新后台数据并显示等,我们大致可以得到的结论是:

  • 需要用到Timer的是UI视图,在其内部使用;
  • 响应的是UI操作,在主线程操作。

问题分析

针对大多数的情况,页面(VC)对视图是强引用,Timer对其target是强引用,再加上Timer会被添加到RunLoop中,因此RunLoop对Timer也是强引用,就算视图对Timer是弱引用,我们也可以得到这样的引用分析图:

iShot2021-05-07 15.09.40.png

在上图中,我们可以看到,就算页面退出,断开VC与View之间的引用,可是RunLoop-Timer-View的引用仍会存在,仍会造成view无法释放,导致VC无法释放,进而产生内存泄露的问题。

这里要排除两种情况,第一种情况是Timer是非重复定时器时,可以在特定的时候设置[timer invalidate]并将timer置为nil,此时便不会产生问题;第二种情况是VC退出时,逐个通知View关闭并移除定时器,如此也可以避免问题产生。

为什么RunLoop-Timer-View引用仍会存在

参考之前的文章:【iOS面试题】1.谈一谈AFNetworking线程保活。在此文章中提到,为子线程RunLoop添加Timer会使得子线程保活;同时由于主线程是随App一直存在并保活的,因此不论Timer是跑在子线程还是主线程,RunLoop对Timer的强引用都是存在的。

加上Timer-View的强引用,便构成了RunLoop-Timer-View的完整引用链条。

如何破解循环引用

破解循环引用的方法有两个:

  • 避免产生循环引用;
  • 在适当时机断开循环引用。 前面提到的循环引用的两种排除情况,充分说明了如何在适当时机断开循环引用,具体可以参考前面的内容。

避免产生循环引用

由于Timer的功能受RunLoop影响,因此要避免产生循环引用,唯一有可能的地方便是在Timer和View之间断开强引用链条。

没有什么是中间对象做不到的,如果有,那就再加一个中间对象

在这里,我们可以使用中间对象将Timer和View之间的强引用断开,分析如下图:

iShot2021-05-07 15.09.59.png

通过中间对象的方式,让Timer强引用中间对象,让中间对象分别弱引用Timer和View,便可以达到目的。代码如下:

中间Timer:

#import <Foundation/Foundation.h>

@interface NSTimer (Intermediate)

+ (NSTimer *)scheduledIntermediateTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

@end
----------------------------
#import "NSTimer+Intermediate.h"
#import "TimerIntermediateObject.h"

@implementation NSTimer (Intermediate)

+ (NSTimer *)scheduledIntermediateTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo {
    TimerIntermediateObject *obj = [[TimerIntermediateObject alloc] init];
    obj.target = aTarget;
    obj.selector = aSelector;
    // 注意这里的target和selector,是指向中间对象和其对应方法的
    obj.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:obj selector:@selector(fire:) userInfo:userInfo repeats:yesOrNo];
    return obj.timer;
}

@end

中间对象:

#import <Foundation/Foundation.h>

@interface TimerIntermediateObject : NSObject

@property (nonatomic, weak) NSTimer *timer;
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;

- (void)fire:(NSTimer *)timer;

@end
----------------------------
#import "TimerIntermediateObject.h"

@implementation TimerIntermediateObject

- (void)fire:(NSTimer *)timer {
    if (self.target) {
        if ([self.target respondsToSelector:self.selector]) {
            [self.target performSelector:self.selector withObject:timer.userInfo];
        }
    } else {
        // 这里利用了weak修饰的对象释放后会自动置为nil的特性进行处理
        [self.timer invalidate];
    }
}

@end

注意使用时直接调用NSTimer的分类方法

代码分析

NSTimer的分类方法接收了原生方法的全套参数,并将指向对象和方法转发给中间对象。

中间对象TimerIntermediateObject的弱引用属性分别接收了timer和target,并在fire:方法内将方法进行中转,并利用了weak修饰的对象释放后会自动置为nil的特性,在target不为nil时调用对应方法,在target为nil时自动将timer失效。

这样便将Timer和View进行了分隔,仍旧搭建了完整业务链条,并且在View被释放后,自动将Timer也失效,将所有资源全部释放,完美的处理了循环引用的问题。