iOS开发基础16-NSTimer 内存泄漏深度解析与解决方案
NSTimer 内存泄漏深度解析与解决方案
NSTimer 是 iOS 中实现周期性任务的常用工具,但如果管理不当,极易导致内存泄漏。本文深入分析 NSTimer 的引用关系,并提供多种解决方案。
一、问题背景
以下代码创建一个每 3 秒打印 "Fire" 的计时器:
@interface DetailViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation DetailViewController
- (IBAction)fireButtonPressed:(id)sender {
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0
target:self
selector:@selector(timerFire:)
userInfo:nil
repeats:YES];
[self.timer fire]; // 立即触发一次
}
- (void)timerFire:(NSTimer *)timer {
NSLog(@"Fire");
}
@end
问题:退出该页面后,计时器仍在后台运行,控制器无法释放。
二、内存泄漏分析
引用关系链
NSTimer 的内存泄漏源于两条强引用链:
RunLoop ──强引用──▶ NSTimer ──强引用──▶ target(self)
-
RunLoop 强引用 NSTimer:
scheduledTimerWithTimeInterval:会自动将 timer 添加到当前 RunLoop 的defaultmode,RunLoop 对其保持强引用。官方文档明确说明:Run loops maintain strong references to their timers, so you don't have to maintain your own strong reference to a timer after you have added it to a run loop.
-
NSTimer 强引用 target:timer 会强引用
target,直到 timer 被invalidate。官方文档:The timer maintains a strong reference to target until it (the timer) is invalidated.
为什么 dealloc 不会执行
由于 timer 强引用了 self,self 的引用计数始终大于 0,控制器的 dealloc 永远不会被调用。而 dealloc 中调用 invalidate 的方案因此完全无效——因为根本走不到 dealloc。
关键结论:在
dealloc中invalidatetimer 无法解决循环引用问题。必须在viewWillDisappear:/viewDidDisappear:或其他主动时机停止 timer。
三、解决方案
方案一:在合适时机手动 invalidate
在视图消失时主动停止 timer,打破 RunLoop → Timer 的引用链:
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}
invalidate会将 timer 从 RunLoop 中移除,RunLoop 释放对 timer 的强引用;同时 timer 释放对 target 的强引用。必须在添加 timer 的同一线程(通常是主线程)调用invalidate。
方案二:弱引用中间目标(Weak Proxy)
创建一个中间对象,timer 强引用中间对象,中间对象弱引用真正的 target,打破 Timer → self 的强引用链。
实现中间目标类
// HWWeakTimerTarget.h
@interface HWWeakTimerTarget : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats;
@end
// HWWeakTimerTarget.m
@implementation HWWeakTimerTarget
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
HWWeakTimerTarget *proxy = [[HWWeakTimerTarget alloc] init];
proxy.target = aTarget;
proxy.selector = aSelector;
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:proxy
selector:@selector(fire:)
userInfo:userInfo
repeats:repeats];
proxy.timer = timer;
return timer;
}
- (void)fire:(NSTimer *)timer {
if (self.target) {
// 将 timer 本身传给 selector,与原生 API 行为一致
[self.target performSelector:self.selector withObject:timer];
} else {
// target 已释放,自动停止 timer
[self.timer invalidate];
}
}
@end
使用
self.timer = [HWWeakTimerTarget scheduledTimerWithTimeInterval:3.0
target:self
selector:@selector(timerFire:)
userInfo:nil
repeats:YES];
- (void)timerFire:(NSTimer *)timer {
NSLog(@"Fire");
}
引用关系:
RunLoop ──强──▶ NSTimer ──强──▶ HWWeakTimerTarget ──弱──▶ self
当 self 被释放后,proxy.target 变为 nil,下次 timer 触发时自动调用 invalidate,整个引用链被打破。
方案三:Block 方式 Timer
iOS 10+ 系统原生 API
iOS 10 起系统提供了 block-based timer,无需手动封装:
Objective-C
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0
repeats:YES
block:^(NSTimer * _Nonnull timer) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) {
[timer invalidate];
return;
}
NSLog(@"Fire");
}];
Swift
weak var weakSelf = self
timer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { timer in
guard let self = weakSelf else {
timer.invalidate()
return
}
print("Fire")
}
block 会被 timer 强引用(通过 userInfo 或内部存储),如果 block 内强引用 self,仍会形成循环引用。必须使用
weakSelf打破引用。
兼容低版本的 Block 封装
如果需要支持 iOS 10 以下,可自行封装 block 版本(原理同方案二,将 block 作为 userInfo 传递给 proxy):
typedef void (^HWTimerHandler)(id userInfo);
@interface HWTimerBlockProxy : NSObject
@property (nonatomic, copy) HWTimerHandler block;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(HWTimerHandler)block
userInfo:(id)userInfo
repeats:(BOOL)repeats;
@end
@implementation HWTimerBlockProxy
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(HWTimerHandler)block
userInfo:(id)userInfo
repeats:(BOOL)repeats {
HWTimerBlockProxy *proxy = [[HWTimerBlockProxy alloc] init];
proxy.block = block;
return [NSTimer scheduledTimerWithTimeInterval:interval
target:proxy
selector:@selector(fire:)
userInfo:userInfo
repeats:repeats];
}
- (void)fire:(NSTimer *)timer {
if (self.block) {
self.block(timer.userInfo);
}
}
@end
方案四:Swift 中的现代方案
Combine 的 Timer.TimerPublisher(iOS 13+)
import Combine
class ViewController: UIViewController {
var cancellable: AnyCancellable?
override func viewDidLoad() {
super.viewDidLoad()
cancellable = Timer.publish(every: 3.0, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.timerFire()
}
}
func timerFire() {
print("Fire")
}
deinit {
cancellable?.cancel() // Combine 自动管理,cancellable 释放时自动取消
}
}
[weak self] 闭包捕获 + AnyCancellable 自动取消,完全避免循环引用。
四、NSTimer 使用注意事项
| 注意点 | 说明 |
|---|---|
| invalidate 时机 | 必须在 viewWillDisappear 等主动时机调用,dealloc 中调用无效 |
| 线程要求 | invalidate 必须在添加 timer 的同一线程调用 |
| RunLoop Mode | scheduledTimer 默认加入 default mode,滚动时(tracking mode)不触发;需滚动时触发用 Timer + common modes |
| repeats = NO | 一次性 timer 触发后自动 invalidate,不会泄漏 |
| CADisplayLink | 与 NSTimer 类似,也存在 target 强引用问题,解决方案相同 |
| weak 属性持有 timer | 可行但不推荐,runloop 持有 timer 期间 weak 有效,但语义不清晰 |
RunLoop Mode 补充
// 默认方式:滚动时暂停
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(fire:) userInfo:nil repeats:YES];
// 滚动时仍触发:手动加入 common modes
self.timer = [NSTimer timerWithTimeInterval:3.0 target:self selector:@selector(fire:) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
五、总结
| 方案 | 原理 | 适用场景 |
|---|---|---|
| 手动 invalidate | 在 viewWillDisappear 中停止 timer | 简单场景,生命周期明确 |
| Weak Proxy | 中间对象弱引用 target,target 释放后自动停止 | 通用方案,兼容低版本 |
| 系统 Block Timer | iOS 10+ 原生 block API + weakSelf | 新项目,最低 iOS 10 |
| Combine Timer | 响应式定时器,cancellable 自动管理 | iOS 13+,Swift 项目 |
- 核心问题:
NSTimer强引用 target,而 RunLoop 强引用 timer,形成RunLoop → Timer → self的引用链。 dealloc中 invalidate 无效,因为循环引用导致 dealloc 永远不会执行。- 推荐组合:iOS 10+ 用系统 block timer + weakSelf,低版本用 Weak Proxy 方案,Swift 项目优先用 Combine。

浙公网安备 33010602011771号