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)
  1. RunLoop 强引用 NSTimerscheduledTimerWithTimeInterval: 会自动将 timer 添加到当前 RunLoop 的 default mode,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.

  2. NSTimer 强引用 target:timer 会强引用 target,直到 timer 被 invalidate。官方文档:

    The timer maintains a strong reference to target until it (the timer) is invalidated.

为什么 dealloc 不会执行

由于 timer 强引用了 selfself 的引用计数始终大于 0,控制器的 dealloc 永远不会被调用。而 dealloc 中调用 invalidate 的方案因此完全无效——因为根本走不到 dealloc

关键结论:在 deallocinvalidate timer 无法解决循环引用问题。必须在 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
posted @ 2015-07-22 12:57  Mr.陳  阅读(670)  评论(0)    收藏  举报