原因
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说明如下:
targetThe object to which to send the message specified by
aSelectorwhen 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中移除(TheRunLoopobject 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。
其他:
本文详细分析了Swift中Timer初始化方法可能导致的循环引用问题,以及由此引发的内存泄漏。针对此问题,提出了四种解决方案:在适当时候释放timer、封装Timer到单独类、使用iOS10.0以上带block的接口、利用NSProxy打破强引用。通过这些方法,可以有效避免ViewController与Timer之间的循环引用,确保内存管理的正确性。
478

被折叠的 条评论
为什么被折叠?



