如何解决Timer的循环引用

Posted CaryaLiu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何解决Timer的循环引用相关的知识,希望对你有一定的参考价值。

原因

Timer的初始化方法中,有如下两种:

/// One
class func scheduledTimer(timeInterval ti: TimeInterval, 
                   target aTarget: Any, 
                 selector aSelector: Selector, 
                 userInfo: Any?, 
                  repeats yesOrNo: Bool) -> Timer
/// Another
init(timeInterval ti: TimeInterval, 
target aTarget: Any, 
selector aSelector: Selector, 
userInfo: Any?, 
repeats yesOrNo: Bool)

其中的参数target说明如下:

target

The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.

这里需要注意的是:

  • Timer对象中会强引用传入的target
  • 如果你使用的上面方法的init版本,则你必须将创建的timer使用add(_:forMode:)加到某个RunLoop中,同时该RunLoop会强引用该timer
  • timerRunLoop中移除(The RunLoop object removes its strong reference to the timer),需要调用timerinvalidate()方法,且该方法的调用须与调用add(_:forMode:)相同的线程。

当你在ViewController中使用初始化的timer属性,由于ViewController强引用timertimertarget又是ViewController,由此造成循环引用。当你在deinit方法中销毁timer, ViewController退出导航栈,发现ViewControllerdeinit方法并没有走,ViewController在等timer释放才会走deinit,而timer的释放在deinit中,导致循环引用无法打破,造成内存泄漏。

解决方案

解决循环引用的问题,归根结底就是要打破循环引用,方法如下:

  1. 在执行deinit方法前释放timer
  2. Timer进行封装隔离
  3. 使用系统其它接口解决(ios 10.0 以上可用)
  4. 使用Timer类对象进行解决
  5. 使用NSProxy进行解决

在执行deinit方法前释放timer

根据业务需求,在执行deinit方法前的合适时机,释放timer。例如:

  • viewWillAppear中创建timer
  • viewWillDisappear中销毁timer

Timer进行封装隔离

这种方法是将Timer封装到另一个类对象中,如下:

@objc protocol RCTimerInterface 
    @objc optional func handleTimer(_ timer: Timer)


class RCTimer 
    private var timer: Timer?
    weak var owner: RCTimerInterface?
    private var timeInterval: TimeInterval = 0
    private var userInfo: Any?
    private var repeats: Bool = false

    init(timeInterval: TimeInterval, userInfo: Any? = nil, repeats yesOrNo: Bool = true, owner: RCTimerInterface? = nil) 
        self.timeInterval = timeInterval
        self.userInfo = userInfo
        self.repeats = yesOrNo
        self.owner = owner
    

    func startTimer() 
        let timer = Timer(timeInterval: timeInterval, target: self, selector: #selector(handleTimer(_:)), userInfo: userInfo, repeats: repeats)
        RunLoop.current.add(timer, forMode: .default)
        self.timer = timer
    

    func stopTimer() 
        timer?.invalidate()
        timer = nil
    

    @objc private func handleTimer(_ timer: Timer) 
        owner?.handleTimer?(timer)
    

ViewController中调用时,如下:

class TimerViewController: UIViewController, RCTimerInterface 
    @IBOutlet weak var timeLabel: UILabel!

    var timer: RCTimer?
    override func viewDidLoad() 
        super.viewDidLoad()

        let timer = RCTimer(timeInterval: 1, owner: self)
        timer.startTimer()
        self.timer = timer
    

    deinit 
        timer?.stopTimer()
        print("\\(#function) invoked.")
    

    override func viewWillAppear(_ animated: Bool) 
        super.viewWillAppear(animated)
        print(#function)
    

    override func viewWillDisappear(_ animated: Bool) 
        super.viewWillDisappear(animated)
        print(#function)
    

    @objc func handleTimer(_ timer: Timer) 
        timeLabel.text = "\\(Date())"
    

这样的情况下,ViewController强引用RCTimer, 但是RCTimer弱引用ViewController, ViewControllerdeinit方法就会调用,在deinit方法中会指定timerinvalidate()方法,从而将timerRunLoop中移除,并且移除对target的强引用,从而打破RCTimer和系统Timer之间的循环引用。

使用系统其它接口解决(iOS 10.0 以上可用)

在iOS10.0及以上版本,新增带block参数的接口:

// One
class func scheduledTimer(withTimeInterval interval: TimeInterval, 
                  repeats: Bool, 
                    block: @escaping (Timer) -> Void) -> Timer
                    
// Two
init(timeInterval interval: TimeInterval, 
repeats: Bool, 
block: @escaping (Timer) -> Void)

// Three
convenience init(fire date: Date, 
        interval: TimeInterval, 
         repeats: Bool, 
           block: @escaping (Timer) -> Void)

以上三个接口都带有block参数,该block的参数中带有自身对象,以避免循环引用。使用时需要注意:

  • 避免block的循环引用
  • 在持有timer对象的类中,记得deinit中调用Timerinvalidate()方法

使用Timer类对象进行解决

这里创建一个Timer的分类,命名为RCTimer, 如下:

extension Timer 
    class func scheduledTimerWithTimerInterval(_ timeInterval: TimeInterval,
                                               repeats: Bool,
                                               block: @escaping () -> Void) -> Timer 
        return Timer.scheduledTimer(timeInterval: timeInterval, target: self, selector: #selector(handleTimer(_:)), userInfo: block, repeats: repeats)
    
    
    @objc class func handleTimer(_ timer: Timer) 
        let block = timer.userInfo as? (()->Void)
        block?()
    

在使用timer时,如下所示:

class TimerViewController: UIViewController 
    @IBOutlet weak var timeLabel: UILabel!

    var timer: Timer?
    override func viewDidLoad() 
        super.viewDidLoad()

        let timer = Timer.scheduledTimerWithTimerInterval(1, repeats: true)  [weak self] in
            self?.timeLabel.text = "\\(Date())"
        
        self.timer = timer
    

    deinit 
        print("\\(#function) invoked.")
    

注意上面使用时,block中需要进行[weak self]处理,如果直接调用self则会造成循环引用。因为 blockself强引用,selftiemr强引用,timer又通过userInfo参数保留block(强引用block),这样就构成了循环引用block->self->timer->userInfo->block,因此要打破这种循环,需要在block中使用self时进行weak处理。

使用NSProxy进行解决

可以使用NSProxy来打破timerViewController之前的强引用。

注:NSProxy相关介绍

由于NSProxy是抽象类,使用Swift继承实现时无法初始化,因此这里采用Objective-C实现,代码如下:

/// RCProxy.h
@interface RCProxy : NSProxy
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id)target;
@end

/// RCProxy.m
@interface RCProxy ()
@property (nonatomic, weak) NSObject *target;
@end

@implementation RCProxy
- (instancetype)initWithTarget:(id)target 
    self.target = target;
    return self;


+ (instancetype)proxyWithTarget:(id)target 
    return [[self alloc] initWithTarget:target];


- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel 
    return [self.target methodSignatureForSelector:sel];


- (void)forwardInvocation:(NSInvocation *)invocation 
    SEL aSelector = [invocation selector];
    if (self.target && [self.target respondsToSelector:aSelector]) 
        [invocation invokeWithTarget:self.target];
    

@end

在创建timer时,如下所示:

class TimerViewController: UIViewController 
    @IBOutlet weak var timeLabel: UILabel!
    var timer: Timer?
    override func viewDidLoad() 
        super.viewDidLoad()
        let timer = Timer.scheduledTimer(timeInterval: 1, target: RCProxy(target: self), selector: #selector(handleTimer(_:)), userInfo: nil, repeats: true)
        self.timer = timer
    

    deinit 
        timer?.invalidate()
        timer = nil
        print("\\(#function) invoked.")
    

    @objc func handleTimer(_ timer: Timer) 
        timeLabel.text = "\\(Date())"
    

上述代码中,需要注意:

  1. 初始化timer时的target参数传递的是RCProxy(target: self)对象
  2. deinit方法中对timer进行了invalidate()处理

拓展

还有哪些系统接口会导致循环引用?

  1. CADisplayLink的创建接口init(target: Any, selector sel: Selector),在创建的CADisplayLink对象内部会对target对象强引用

  2. block参数的消息通知注册:

func addObserver(forName name: NSNotification.Name?,           object obj: Any?,            queue: OperationQueue?,            using block: @escaping (Notification) -> Void) -> NSObjectProtocol

上面的接口会返回消息监听者observer, 当self持有observer时,在block内部使用self就需要进行weak处理。并且一定要记住移除observer。

其他:

为什么Timer使用weak不能打破循环引用

了解NSProxy

以上是关于如何解决Timer的循环引用的主要内容,如果未能解决你的问题,请参考以下文章

如何解决Timer的循环引用

🔥🔥iOS中解决NSTimer循环引用问题

解决NSTimer或CADisplayLink计时器造成的循环引用问题。

NSTimer解除循环引用

iOS NSTimer 的循环引用问题

iOS NSTimer 的循环引用问题