如何解决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
- 将
timer
从RunLoop
中移除(TheRunLoop
object removes its strong reference to the timer),需要调用timer
的invalidate()
方法,且该方法的调用须与调用add(_:forMode:)
在相同的线程。
当你在ViewController
中使用初始化的timer
属性,由于ViewController
强引用timer
,timer
的target
又是ViewController
,由此造成循环引用。当你在deinit
方法中销毁timer
, ViewController
退出导航栈,发现ViewController
的deinit
方法并没有走,ViewController
在等timer
释放才会走deinit
,而timer
的释放在deinit
中,导致循环引用无法打破,造成内存泄漏。
解决方案
解决循环引用的问题,归根结底就是要打破循环引用,方法如下:
- 在执行
deinit
方法前释放timer
- 对
Timer
进行封装隔离 - 使用系统其它接口解决(ios 10.0 以上可用)
- 使用
Timer
类对象进行解决 - 使用
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
, ViewController
的deinit
方法就会调用,在deinit
方法中会指定timer
的invalidate()
方法,从而将timer
从RunLoop
中移除,并且移除对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
中调用Timer
的invalidate()
方法
使用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
则会造成循环引用。因为 block
对self
强引用,self
对tiemr
强引用,timer
又通过userInfo
参数保留block
(强引用block
),这样就构成了循环引用block->self->timer->userInfo->block
,因此要打破这种循环,需要在block中使用self
时进行weak
处理。
使用NSProxy
进行解决
可以使用NSProxy
来打破timer
和ViewController
之前的强引用。
由于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())"
上述代码中,需要注意:
- 初始化
timer
时的target
参数传递的是RCProxy(target: self)
对象 - 在
deinit
方法中对timer
进行了invalidate()
处理
拓展
还有哪些系统接口会导致循环引用?
-
CADisplayLink
的创建接口init(target: Any, selector sel: Selector)
,在创建的CADisplayLink
对象内部会对target
对象强引用 -
带
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的循环引用的主要内容,如果未能解决你的问题,请参考以下文章