iOS开发基础16-使用 `NSTimer` 时避免内存泄露的技巧和最佳实践
在iOS开发中,NSTimer
是一个常用的工具,用于实现周期性任务。然而,在使用过程中,如果不注意管理内存,容易导致内存泄露问题,特别是当 NSTimer
针对 self
执行回调时。这篇文章将详细介绍如何通过一些技巧和封装来避免这些问题。
问题背景
以下代码创建了一个计时器,每隔3秒钟在控制台输出一次 "Fire":
@interface DetailViewController ()
@property (nonatomic, weak) NSTimer *timer;
@end
@implementation DetailViewController
- (IBAction)fireButtonPressed:(id)sender {
_timer = [NSTimer scheduledTimerWithTimeInterval:3.0f
target:self
selector:@selector(timerFire:)
userInfo:nil
repeats:YES];
[_timer fire];
}
- (void)timerFire:(id)userinfo {
NSLog(@"Fire");
}
@end
尽管上述代码可以正常工作,但我们发现,即使跳转到其他界面,计时器依然在后台继续运行。显然,计时器没有被正确释放。
内存泄露分析
强引用的陷阱
-
NSTimer
被RunLoop
强引用:当NSTimer
被添加到RunLoop
中时,它会被RunLoop
强引用,从而不需要你手动保留它。Note in particular that 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
:一个更为重要的点是,NSTimer
会强引用其target
,即使target
是self
。Target is the object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.
因此,如果 target
是 self
,NSTimer
会保持对 self
的强引用,导致 self
不能被释放,从而不会调用 dealloc
方法来释放计时器。
解决方案
1. 手动释放 NSTimer
在视图控制器的 dealloc
方法中手动调用 invalidate
来释放计时器:
- (void)dealloc {
[_timer invalidate];
NSLog(@"%@ dealloc", NSStringFromClass([self class]));
}
此方法虽然直截了当,但需要注意在合适的时机调用 invalidate
,特别是计时器所在的线程。
2. 使用弱引用目标(HWWeakTimerTarget
)
为了避免 NSTimer
强引用 self
,可以创建一个中间目标来打破这种强引用关系。
实现中间目标
首先,定义一个中间目标类:
@interface HWWeakTimerTarget : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer *timer;
@end
@implementation HWWeakTimerTarget
- (void)fire:(NSTimer *)timer {
if (self.target) {
[self.target performSelector:self.selector withObject:timer.userInfo];
} else {
[self.timer invalidate];
}
}
@end
定义一个新的 scheduledTimerWithTimeInterval
方法
封装一个新的方法来创建 NSTimer
,通过使用 HWWeakTimerTarget
:
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {
HWWeakTimerTarget *timerTarget = [[HWWeakTimerTarget alloc] init];
timerTarget.target = aTarget;
timerTarget.selector = aSelector;
timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:timerTarget
selector:@selector(fire:)
userInfo:userInfo
repeats:repeats];
return timerTarget.timer;
}
使用新的方法创建 NSTimer
- (IBAction)fireButtonPressed:(id)sender {
_timer = [HWWeakTimer scheduledTimerWithTimeInterval:3.0f
target:self
selector:@selector(timerFire:)
userInfo:nil
repeats:YES];
[_timer fire];
}
3. 使用 Block 方式的NSTimer
通过 Block 方式来创建 NSTimer
,保持代码简洁:
实现 Block 版本的 NSTimer
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(HWTimerHandler)block
userInfo:(id)userInfo
repeats:(BOOL)repeats {
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(_timerBlockInvoke:)
userInfo:@[[block copy], userInfo]
repeats:repeats];
}
+ (void)_timerBlockInvoke:(NSArray *)userInfo {
HWTimerHandler block = userInfo[0];
id info = userInfo[1];
if (block) {
block(info);
}
}
使用 Block 创建 NSTimer
- (IBAction)fireButtonPressed:(id)sender {
_timer = [HWWeakTimer scheduledTimerWithTimeInterval:3.0f
block:^(id userInfo) {
NSLog(@"%@", userInfo);
}
userInfo:@"Fire"
repeats:YES];
[_timer fire];
}
总结
通过上述方式,可以有效避免在使用 NSTimer
时的内存泄露问题。关键在于:
- 理解
NSTimer
和RunLoop
的强引用关系。 - 使用中间目标类(
HWWeakTimerTarget
)打破NSTimer
对self
的强引用。 - 使用 Block 方式简化计时器的创建和管理。
这篇文章能帮助你更好地理解和使用 NSTimer
,避免常见的内存泄露问题。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!