资讯专栏INFORMATION COLUMN

iOS Timer 盘点

Shisui / 865人阅读

摘要:苹果提供给了我们好几种可以达到效果的方法,我尝试在这里盘点一下。依据苹果文档中提到内容,运行的模式是,那么,滚动的时候,会切换,自然不会被回调。能否正确释放返回值和不同的是,并不会出现的警告。

博文地址:http://ifujun.com/ios-timer-pan-dian/

在iOS的开发过程中,Timer是一个很常见的功能。苹果提供给了我们好几种可以达到Timer效果的方法,我尝试在这里盘点一下。

NSTimer

NSTimer是我们最常见的一种Timer,我们从NSTimer开始说起。

用法

NSTimer的用法很简单,个人比较常用的是下面这个方法:

[NSTimer scheduledTimerWithTimeInterval:1.0f
                                     target:self
                                   selector:@selector(test)
                                   userInfo:nil
                                    repeats:nil];
Tips 为何停止?

有这么一道面试题,题目是这样的:

UITableViewCell上有个UILabel,显示NSTimer实现的秒表时间,手指滚动cell过程中,label是否刷新,为什么?

我们来试验一下:

通过试验,我们发现,在手拖拽或者滑动的过程中,label并没有更新,NSTimer也没有循环。

那这是为什么呢?这与RunLoop有关。在NSTimer的官方文档上,苹果是这么说的:

A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed.

意思就是说,NSTimer并不是一种实时机制,它只会在下面条件满足的情况下才会启动:

NSTimer被添加到的RunLoop模式正在运行。

NSTimer设定的启动时间还没有过去。

关键在于第一点,我们刚才的NSTimer默认添加在NSDefaultRunLoopMode上,而UIScrollView在滑动的时候,RunLoop会自动切换到 UITrackingRunLoopModeNSTimer并没有添加到这个RunLoop模式上,自然是不会启动的。

所以,如果我们想要NSTimerUIScrollView滑动的时候也会启动的话,只要将NSTimer添加到NSRunLoopCommonModes上即可。NSRunLoopCommonModes是RunLoop模式的集合。

我们试验一下:

self.timer = [NSTimer scheduledTimerWithTimeInterval:0.8f
                                              target:self
selector:@selector(autoIncrement)
                                            userInfo:nil
                                             repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

是否精确?

在上面的为何停止的分析中,我们了解到,NSTimer只会在它所加入的RunLoop上启动和循环。如果在类似于上面的情况,而NSTimer又只是加入NSDefaultRunLoopMode的话,这时候的NSTimer在切换RunLoop模式之后,必然没有精确可言。

那么,如果RunLoop一直运行在NSTimer被加入到的模式上,或者加入到的是NSRunLoopCommonModes的模式上,是否就是精确的呢?

首先,我们假设线程正在进行一个比较大的连续运算,这时候,我们的NSTimer会被准时启动吗?

我在程序中每0.5秒打印一下”test”,然后用很慢的办法去计算质数,运行结果如下:

在计算质数的过程中,线程完全阻塞,并不打印”test”,等到执行完成才开始打印,第一个”start”和第二个”test”中间间隔了13秒。

所以NSTimer是否精确,很大程度上取决于线程当前的空闲情况。

除此之外,还有一点我想提及一下。

在NSTimer的头文件中,苹果新增了一个属性,叫做tolerance,我们可以理解为容差。苹果的意思是如果设定了tolerance值,那么:

设定时间 <= NSTimer的启动时间 <= 设定时间 + tolerance

那么,这个有什么用呢,因为一般来说,我们想要的就是精确。苹果的解释是:

Setting a tolerance for a timer allows it to fire later than the scheduled fire date, improving the ability of the system to optimize for increased power savings and responsiveness.

意思就是,设定容差可以起到省电和优化系统响应性的作用。

tolerance如果不设定的话,默认为0。那么,是否一定可以精确呢?苹果在头文件中提到了这么一点:

The system reserves the right to apply a small amount of tolerance to certain timers regardless of the value of this property.

意思就是,哪怕为0,系统依然有权利去设置一个很小的容差。

Even a small amount of tolerance will have a significant positive impact on the power usage of your application.

毕竟一个很小的容差都可以对电量产生一个很大的积极的影响。

所以,从上面的论述中我们可以看到,即使RunLoop模式正确,当前线程并不阻塞,系统依然可能会在NSTimer上加上很小的的容差。

如何终止?

NSTimer提供了一个invalidate方法,用于终止NSTimer。但是,这里涉及到一个多线程的问题。假设,我在A线程启动NSTimer,在B线程调用invalidate方法来终止NSTimer,那么,NSTimer是否会终止呢。

我们来试验一下:

dispatch_async(dispatch_get_main_queue(), ^{
        self.timer = [NSTimer scheduledTimerWithTimeInterval:0.5f
                                                      target:self
                                                    selector:@selector(test)
                                                    userInfo:nil
                                                     repeats:YES];
    });
    
dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self.timer invalidate];
    });

结果是并不会停止

在NSTimer的官方文档上,苹果是这么说的:

You should always call the invalidate method from the same thread on which the timer was installed.

所以,我们必须哪个线程调用,哪个线程终止。

CFRunLoopTimerRef

在NSTimer的官方文档上,苹果提到:

NSTimer is “toll-free bridged” with its Core Foundation counterpart, CFRunLoopTimerRef. See Toll-Free Bridgingfor more information on toll-free bridging.

NSTimer可以直接桥接到CFRunLoopTimerRef上,两者的数据结构是相同的,是可以互换的。我们可以理解为,NSTimer是objc版本的CFRunLoopTimerRef封装。

用法

CFRunLoopTimerRef一般用法如下:

NSTimeInterval fireDate = CFAbsoluteTimeGetCurrent();
CFRunLoopTimerRef timer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, fireDate, 0.5f, 0, 0, ^(CFRunLoopTimerRef timer) {
        NSLog(@"test");
    });
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes);

其他用法可以参考苹果的CFRunLoopTimerRef文档。

Tips 和RunLoop有什么关系?

CoreFoundation框架中有主要有5个RunLoop类:

CFRunLoopRef

CFRunLoopModeRef

CFRunLoopSourceRef

CFRunLoopTimerRef

CFRunLoopObserverRef

CFRunLoopTimerRef属于其中之一。

关于RunLoop,ibireme大神写了一个非常好的文章,大家可以围观学习一下:

http://blog.ibireme.com/2015/05/18/runlo...

有没有需要使用CFRunLoopTimerRef?

既然CFRunLoopTimer is “toll-free bridged” with its Cocoa Foundation counterpart, NSTimer. ,那么如果在使用CF框架写内容的话,可以直接使用,否则,还是使用NSTimer吧。

dispatch_after 用法

dispatch_after的用法比较简单,一般设定一个时间,然后指定一个队列,比如Main Dispatch Queue

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"test");
    });

和上面说到的NSTimerCFRunLoopTimerRef不同,NSTimerCFRunLoopTimerRef是在指定的RunLoop上注册启动时间,而dispatch_after是在指定的时间后,将整个执行的Block块添加到指定队列的RunLoop上。

所以,如果此队列处于繁重任务或者阻塞之中,dispatch_after的Block块肯定是要延后执行的。

Tips 强引用

假设现在有这么一个情况,dispatch_after中引用了Self,那么在设定时间之前,Self可以被释放吗?

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.label.text = @"ok";
    });

我来试验一下,我从A-VC push到B-VC,之后再pop回来,B-VC有个5秒的dispatch_after:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        self.label.text = @"ok";
        NSLog(@"-- %@",self.description);
    });

我们看一下输出:

结果是,在pop回去之后,Self并没有得到释放,在dispatch_after的Block块执行完成之后,Self才得到正确释放。

如何解决dispatch_after的Block块中的强引用问题?

MLeaksFinder是一个开源的iOS内存泄露检测工具。作者很有想法,他在ViewController被pop或dismiss之后,在一定时间之后(默认为3秒),去看看这个ViewController和它的subviews是否还存在。

基本上是类似于这样:

__weak id weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf assertNotDealloc];
    });

这里如果直接使用Self的话,ViewController被pop或者dismiss之后,依然是无法释放的,而__weak就可以解决这个问题。在这里,Self如果被正确释放的话,weakSelf自然会变成nil

我们修改一下我们试验的代码:

__weak typeof(self) weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        weakSelf.label.text = @"ok";
        NSLog(@"-- %@",weakSelf.description);
    });

OK,没有问题。

performSelector:withObject:afterDelay: 用法

这依然是对CFRunLoopTimerRef的封装,和NSTimer同源。既然是同源,那么同样具有NSTimer的RunLoop特性。依据苹果文档中提到内容,performSelector:withObject:afterDelay:运行的RunLoop模式是NSDefaultRunLoopMode,那么,ScrollView滚动的时候,RunLoop会切换,performSelector:withObject:afterDelay:自然不会被回调。

This method sets up a timer to perform the aSelector message on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode).

performSelector:withObject:afterDelay:的用法也比较简单。

[self performSelector:@selector(test) withObject:nil afterDelay:1.0f];
Tips 强引用

假设是Self去调用performSelector:withObject:afterDelay:方法,在Delay时间未到之前,Self能否被释放呢?

我们试验一下:

[self performSelector:@selector(printDescription) withObject:nil afterDelay:5.0f];

结果和上面的dispatch_after一样,我们修改一下代码,再看一下:

__weak typeof(self) weakSelf = self;
[weakSelf performSelector:@selector(printDescription) withObject:nil afterDelay:5.0f];

很遗憾,没有用。但是我们可以取消这个performSelector

这种方法可以取消Target指定的带执行的方法。

[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(printDescription) object:nil];

这种方法可以取消Target的所有待执行的方法。

[NSObject cancelPreviousPerformRequestsWithTarget:self];
能否正确释放返回值?

performSelector不同的是,performSelector:withObject:afterDelay:并不会出现”PerformSelector may cause a leak because its selector is unknown.”的警告。

那么,这是否意味着performSelector:withObject:afterDelay:可以正确释放返回值呢?

如果现在performSelector:withObject:afterDelay:所执行的Selector并不确定,并且可能会返回一个对象,那么系统能否正确释放这个返回值呢?

我们试验一下,这里printDescriptionAprintDescriptionB方法各会返回一个不同类型的View(此View是新建的对象),printDescriptionC会返回Void

NSArray *array = @[@"printDescriptionA",
                   @"printDescriptionB",
                   @"printDescriptionC"];
        
NSString *selString = array[arc4random()%3];
NSLog(@"sel = %@", selString);
SEL tempSel = NSSelectorFromString(selString);
if ([self respondsToSelector:tempSel])
{
    [self performSelector:tempSel withObject:nil afterDelay:3.0f];
}

几次尝试之后,我发现,这是可以正常释放的。

在Effective Objective-C 2.0一书中,作者在第42条上提到:

performSelector系列方法在内存管理方面容易有疏失。它无法确定要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法。

关于这个问题,stackoverflow上也有很多讨论:

http://stackoverflow.com/questions/70172...

我不知道如何触发这种内存泄露,有知道的告诉我一声,学习一下,谢谢。

参考资料

https://developer.apple.com/library/ios/...

https://developer.apple.com/library/ios/...

https://developer.apple.com/library/mac/...

https://developer.apple.com/library/ios/...

http://blog.ibireme.com/2015/05/18/runlo...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/16200.html

相关文章

  • fir.im Weekly - iOS / Android 动态化更新方案盘点

    摘要:本期整理了滴滴打车团队动态化方案的作者动态化分享,以及空间微信微信读书美团点评技术团队的热修复热更新方案,还有一些其他的技术分享。微信文章传送门滴滴动态化方案的诞生与起航,同时,滴滴客户端架构团队考虑于年初开源。 动态化更新是 App 开发必然面对的问题。在 iOS 环境下,Apple 开发者们像是 带着手铐脚镣跳舞 ,相比之下 Android 开发者会轻松一点,有很多相关的开源框架帮...

    Yuqi 评论0 收藏0
  • fir.im Weekly - iOS / Android 动态化更新方案盘点

    摘要:本期整理了滴滴打车团队动态化方案的作者动态化分享,以及空间微信微信读书美团点评技术团队的热修复热更新方案,还有一些其他的技术分享。微信文章传送门滴滴动态化方案的诞生与起航,同时,滴滴客户端架构团队考虑于年初开源。 动态化更新是 App 开发必然面对的问题。在 iOS 环境下,Apple 开发者们像是 带着手铐脚镣跳舞 ,相比之下 Android 开发者会轻松一点,有很多相关的开源框架帮...

    genefy 评论0 收藏0
  • fir.im Weekly - 热门 iOS 第三方库大盘点

    摘要:源码地址在此开源了在微博上分享开源了。中炸裂特效的实现分析说道前几天微博上被一个很优秀的开源组件刷屏了,效果非常酷炫,有点类似卸载时的动画,先来感受一下。 本期 fir.im Weekly 收集的热度资源,大部分关于Android、iOS 开发工具、源码和脑洞大开的 UI 动画,希望给你带来更多的工作创意与灵感。 盘点国内程序员不常用的热门iOS第三方库 @ios122 的这份整理综合...

    luoyibu 评论0 收藏0
  • fir.im Weekly - 热门 iOS 第三方库大盘点

    摘要:源码地址在此开源了在微博上分享开源了。中炸裂特效的实现分析说道前几天微博上被一个很优秀的开源组件刷屏了,效果非常酷炫,有点类似卸载时的动画,先来感受一下。 本期 fir.im Weekly 收集的热度资源,大部分关于Android、iOS 开发工具、源码和脑洞大开的 UI 动画,希望给你带来更多的工作创意与灵感。 盘点国内程序员不常用的热门iOS第三方库 @ios122 的这份整理综合...

    Lavender 评论0 收藏0
  • #yyds干货盘点# STM32 Stm32Cube配置外部IO中断

    摘要:电源控制状态寄存器中的用来表明是高于还是低于的电压阀值。当下降到阀值以下或上升到阀值之上时,通过外部中断线上升或下降边沿触发设置,产生中断。在中断处理函数中做相应的保护措施。 在单片机低功耗的应用中,我们常常需要使用外部中断,在无中断的情况下进行休眠,这里讲解一下使用STM32 Stm32Cube配置外部IO中断,将io口...

    番茄西红柿 评论0 收藏2637

发表评论

0条评论

最新活动
阅读需要支付1元查看
<