以前的老代码在使用 NSTimer 时出现了内存泄露

NSTimer

fire

我们先用 NSTimer 来做个简单的计时器,每隔5秒钟在控制台输出 Fire 。比较想当然的做法是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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

运行之后确实在控制台每隔3秒钟输出一次 Fire ,然而当我们从这个界面跳转到其他界面的时候却发现:控制台还在源源不断的输出着 Fire 。看来 Timer 并没有停止。

invalidate

既然没有停止,那我们在 DemoViewController 的 dealloc 里加上 invalidate 的方法:

1
2
3
4
-(void)dealloc {
    [_timer invalidate];
    NSLog(@"%@ dealloc", NSStringFromClass([self class]));
}

再次运行,还是没有停止。原因是 Timer 添加到 Runloop 的时候,会被 Runloop 强引用:

1
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.

然后 Timer 又会有一个对 Target 的强引用(也就是 self ):

1
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.

也就是说 NSTimer 强引用了 self ,导致 self 一直不能被释放掉,所以也就走不到 self 的 dealloc 里。

既然如此,那我们可以再加个 invalidate 按钮:

1
2
3
- (IBAction)invalidateButtonPressed:(id)sender {
    [_timer invalidate];
}

嗯这样就可以了。(在 SOF 上有人说该在 invalidate 之后执行 _timer = nil ,未能理解为什么,如果你知道原因可以告诉我:)

在 invalidate 方法的文档里还有这这样一段话:

You must send this message from the thread on which the timer was installed. If you send this message from another thread, the input source associated with the timer may not be removed from its run loop, which could prevent the thread from exiting properly.

NSTimer 在哪个线程创建就要在哪个线程停止,否则会导致资源不能被正确的释放。看起来各种坑还不少。

dealloc

那么问题来了:如果我就是想让这个 NSTimer 一直输出,直到 DemoViewController 销毁了才停止,我该如何让它停止呢?

  • NSTimer 被 Runloop 强引用了,如果要释放就要调用 invalidate 方法。

  • 但是我想在 DemoViewController 的 dealloc 里调用 invalidate 方法,但是 self 被 NSTimer 强引用了。

  • 所以我还是要释放 NSTimer 先,然而不调用 invalidate 方法就不能释放它。

  • 然而你不进入到 dealloc 方法里我又不能调用 invalidate 方法。

  • 嗯…

HWWeakTimer

weakSelf

问题的关键就在于 self 被 NSTimer 强引用了,如果我们能打破这个强引用问题自然而然就解决了。所以一个很简单的想法就是:weakSelf:

1
2
3
4
5
6
__weak typeof(self) weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:3.0f
                                          target:weakSelf
                                        selector:@selector(timerFire:)
                                        userInfo:nil
                                         repeats:YES];

然而这并没有什么卵用,这里的 __weak 和 __strong 唯一的区别就是:如果在这两行代码执行的期间 self 被释放了, NSTimer 的 target 会变成 nil 。

target

既然没办法通过 __weak 把 self 抽离出来,我们可以造个假的 target 给 NSTimer 。这个假的 target 类似于一个中间的代理人,它做的唯一的工作就是挺身而出接下了 NSTimer 的强引用。类声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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 方法,但是在调用的时候已经偷梁换柱了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+ (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;
}

再次运行,问题解决。

block

如果能用 block 来调用 NSTimer 那岂不是更好了。我们可以这样来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (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];
    // or `!block ?: block();` @sunnyxx
    if (block) {
        block(info);
    }
}

这样我们就可以直接在 block 里写相关逻辑了:

1
2
3
4
5
6
- (IBAction)fireButtonPressed:(id)sender {
    _timer = [HWWeakTimer scheduledTimerWithTimeInterval:3.0f block:^(id userInfo) {
        NSLog(@"%@", userInfo);
    } userInfo:@"Fire" repeats:YES];
    [_timer fire];
}

嗯就是这样。

iOS开发——高级篇——iOS 中的 NSTimer的更多相关文章

  1. iOS开发——高级篇——iOS开发之网络安全密码学

    一.非对称加密 - RSA : + 公钥加密,私钥解密: + 私钥加密,公钥解密: + 只能通过因式分解来破解 二.对称加密 - DES - 3DES - AES (高级密码标准,美国国家安全局使用, ...

  2. iOS开发——高级篇——iOS 项目的目录结构

    最近闲来无事去面试一下iOS开发,让我感到吃惊的,面试官竟然问怎么分目录结构,还具体问每个子目录的文件名. 目录结构确实非常重要,面试官这么问,无疑是想窥探开发经验.清晰的目录结构,可让人一眼明白相应 ...

  3. iOS开发——高级篇——iOS中如何选择delegate、通知、KVO(以及三者的区别)

      在开发IOS应用的时候,我们会经常遇到一个常见的问题:在不过分耦合的前提下,controllers[B]怎么进行通信.在IOS应用不断的出现三种模式来实现这种通信:1委托delegation2通知 ...

  4. iOS开发——高级篇——iOS中常见的设计模式(MVC/单例/委托/观察者)

    关于设计模式这个问题,在网上也找过一些资料,下面是我自己总结的,分享给大家 如果你刚接触设计模式,我们有好消息告诉你!首先,多亏了Cocoa的构建方式,你已经使用了许多的设计模式以及被鼓励的最佳实践. ...

  5. iOS开发——高级篇——iOS中为什么block用copy属性

    1. Block的声明和线程安全Block属性的声明,首先需要用copy修饰符,因为只有copy后的Block才会在堆中,栈中的Block的生命周期是和栈绑定的,可以参考之前的文章(iOS: 非ARC ...

  6. iOS开发——高级篇——iOS 强制退出程序APP代码

    1.先po代码 UIAlertView* alert = [[UIAlertView alloc] initWithTitle:self.exitapplication message:@" ...

  7. iOS开发——高级篇——iOS如何彻底避免数组越界

    我们先来看看有可能会出现的数组越界Crash的地方: ? 1 2 3 4 5 6 7 - (void)tableView:(UITableView *)tableView didSelectRowAt ...

  8. iOS开发——高级篇——iOS键盘的相关设置(UITextfield)

    一.键盘风格 UIKit框架支持8种风格键盘. typedef enum { UIKeyboardTypeDefault, // 默认键盘:支持所有字符 UIKeyboardTypeASCIICapa ...

  9. iOS开发——高级篇——iOS涂鸦画板效果实现

    一个简单的绘图应用,模仿苹果自带软件备忘录里的涂鸦功能 核心代码 #import "DrawView.h" #import "DrawPath.h" @inte ...

随机推荐

  1. Spring-data-jpa 的@modifying注解

    在项目的进行中需要通过 @Modifying 注解完成修改操作(注意:不支持新增) 1.之前有一个业务需要先查询一个结果集,然后将满足结果集中某个条件的另外一张表中的字段做一个属性值的更改,这个更改可 ...

  2. ASP------如何使界面布局具有一致外观

    使用布局页或布局块的方法 转载: http://www.runoob.com/aspnet/webpages-layout.html

  3. 9月13日JavaScript语句循环(100以备奇偶数、100以内与7先关的数、100以内整数的和、10以内阶乘、乘法口诀、篮球弹起高度、64格子放东西)

    3.循环 循环是操作某一个功能(执行某段代码). ①循环四要素: a 循环初始值 b 循环的条件 c 循环状态 d 循环体 ②for循环 a 穷举:把所有的可能性的都一一列出来. b 迭代:每次循环都 ...

  4. (转)Rest介绍

    参考文献:Rest简介 REST是一种组织Web服务的架构,其只在架构方面提出了一系列约束. 关于Restful的无状态 所以在stackoverflow中,我们常常会看到有人问:我现在使用了这样一种 ...

  5. 使用ASP.NET Web Api构建基于REST风格的服务实战系列教程【九】——API变了,客户端怎么办?

    系列导航地址http://www.cnblogs.com/fzrain/p/3490137.html 前言 一旦我们将API发布之后,消费者就会开始使用并和其他的一些数据混在一起.然而,当新的需求出现 ...

  6. Eclipse查看hadoop源代码出现Source not found,是因为没有添加.zip

    在我们hadoop编程中,经常遇到像看看hadoop的某个类中函数的功能.但是我们会遇到一种情况就是Source not found.遇到这个问题,该如何解决.因为我们已经引入了包,为什么会找不到.如 ...

  7. Vim编辑器

    vim的学习曲线相当的大(参看各种文本编辑器的学习曲线),所以,如果你一开始看到的是一大堆VIM的命令分类,你一定会对这个编辑器失去兴趣的.下面的文章翻译自<Learn Vim Progress ...

  8. PNG类库

    libpng depends on Zlib http://www.libpng.org/pub/png/libpng.html LodePNG http://lodev.org/lodepng/ P ...

  9. dedecms标签的sql语句

    {dede:sql sql='Select content from dede_arctype where id=1' titlelen='40′} [field:content/] {/dede:s ...

  10. MySQL注入

    SQL Injection Tutorial by Marezzi (MySQL) SQL注入教程由Marezzi(MySQL的) In this tutorial i will describe h ...