从NSTimer的失效性谈起(一):关于NSTimer和NSRunLoop

简介:

一、NSTimer的失效性

在iOS中要设置一个定时器的通常做法是调用如下API:

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

这个API会创建一个NSTimer对象,将其添加到当前runloop的defaultMode中,然后返回该对象,如下所说:

Creates and returns a new NSTimer object and schedules it on the current run loop in the default mode.

由于NSTimer的过期事件需要由NSRunLoop来执行:

而在次线程上我们还需要额外显式地开启次线程所对应的runloop(比较麻烦),所以通常我们都会在主线程直接调用该API来设置并启动一个定时器。

一个NSRunLoop有几种mode,目前我们接触比较多的是NSDefaultRunLoopModeUITrackingRunLoopMode,而NSRunLoopCommonModes更多类似于一个标志,可以通过如下API将某一种mode归进commonModes:

CF_EXPORT void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode);

这样一来,我们就可以通过将定时器添加到NSRunLoopCommonModes来一次性添加到多个对应的mode中。

通常,我们添加到defaultMode的定时器是可以正常工作的,不过当用户对scrollView进行滑动时定时器就失效了。这是因为此时mainRunLoop从NSDefaultRunLoopMode退出,而进入到了UITrackingRunLoopMode,所以添加到前者的定时器不会得到处理。

二、验证mainRunLoop的mode变化

关于NSRunLoop的进一步探究,可以参考:Run LoopsNSRunLoop InternalsCFRunLoop.c深入理解RunLoop

我们可以通过RunLoopObserver来观察一个runloop的mode切换:

    CFRunLoopObserverRef observerRef = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        if (activity == kCFRunLoopExit) {
            NSLog(@"runloop mode %@ exit\n", [[NSRunLoop currentRunLoop] currentMode]);
        } else if (activity == kCFRunLoopEntry) {
            NSLog(@"runloop mode %@ entry\n", [[NSRunLoop currentRunLoop] currentMode]);
        }
    });
    CFRunLoopAddObserver(CFRunLoopGetMain(), observerRef, kCFRunLoopCommonModes);

之前我猜想:对于mainRunLoop来说,一旦进入就不会退出了,除非应用程序结束。经过模拟,可以发现在滑动scrollView的时候会触发kCFRunLoopExitkCFRunLoopEntry事件,断点得到的调用栈如下图:

entry

通过上图调用栈来对应CFRunLoop.c源码可以找到相应方法调用:

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    ... // 省略部分代码

    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    ... // 省略部分代码
    return result;
}

可以看到在CFRunLoopRunSpecific这一层对应的是kCFRunLoopEntrykCFRunLoopExit的回调,而具体事件处理则落在了__CFRunLoopRun内:

/* rl, rlm are locked on entrance and exit */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    uint64_t startTSR = mach_absolute_time();
    ... // 省略部分代码

    dispatch_source_t timeout_timer = NULL;
    ... // 省略部分代码

    int32_t retVal = 0;
    do {
        ... // 省略部分代码
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

        __CFRunLoopDoBlocks(rl, rlm);
        ... // 省略部分代码

        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        ... // 省略部分代码

        if (!poll) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        ... // 省略部分代码

        if (!poll) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        ... // 省略部分代码
    } while (0 == retVal);
    return retVal;
}

可以看到在__CFRunLoopRun中负责剩余几种状态的回调:kCFRunLoopBeforeTimerskCFRunLoopBeforeSourceskCFRunLoopBeforeWaitingkCFRunLoopAfterWaiting,对应着如下调用栈:

beforeTimer

综上可以验证在开始滚动和停止滚动的时候,mainRunLoop确实会进行mode的切换。开始滚动时从NSDefaultRunLoopMode切换到UITrackingRunLoopMode(可能是为了让滚动更顺畅?),停止滚动时则反过来。

而不同mode维护着各自的inputSources和timerSources,所以在UITrackingRunLoopMode下不会处理添加到NSDefaultRunLoopMode的定时器。

目录
相关文章
|
10月前
|
Python
探索LightGBM:异常值处理与鲁棒建模
探索LightGBM:异常值处理与鲁棒建模【2月更文挑战第2天】
322 0
|
10月前
移动端click事件、touch事件、tap事件的区别
移动端click事件、touch事件、tap事件的区别
272 0
|
10月前
|
存储 SQL 定位技术
数据库基础(五):存储过程与触发器的创建、执行、修改、删除
数据库基础(五):存储过程与触发器的创建、执行、修改、删除
303 1
|
Web App开发 Java 开发工具
systrace: 系统级跟踪工具的解析
systrace是Android4.1版本之后推出的,对系统Performance分析的工具,该工具结合Android 内核的数据,最终会生产html文件。 systrace的功能包括跟踪系统的I/O操作、内核工作队列、CPU负载以及Android各个子系统的运行状况等
|
Dart JavaScript Java
腾讯 Matrix 增量编译 bug 解决,PR 已被官方采访(一)
腾讯 Matrix 增量编译 bug 解决,PR 已被官方采访
|
存储 监控 Ubuntu
汽车以太网交换机的设计
汽车以太网交换机的设计
汽车以太网交换机的设计
|
SQL 消息中间件 移动开发
使用MyBatisPlus实现多租户功能
使用MyBatisPlus实现多租户功能
使用MyBatisPlus实现多租户功能
MPAndroidChart 教程:与图表的交互 Interaction with the Chart
该库允许您完全自定义与图表视图的可能触摸(和手势)交互,并通过回调方法对交互作出反应。 启用/禁用交互 setTouchEnabled(boolean enabled):启用/禁用与图表的所有可能的触摸交互。
1182 0
|
缓存 算法 NoSQL
微服务怎么限流?算法+框架+实战!
背景 随着微服务的流行,服务和服务之间的稳定性变得越来越重要。缓存、降级和限流是保护微服务系统运行稳定性的三大利器。 缓存:提升系统访问速度和增大系统能处理的容量 降级:当服务出问题或者影响到核心流程的性能则需要暂时屏蔽掉 限流:解决服务雪崩,级联服务发生阻塞时,及时熔断,防止请求堆积消耗占用系统的线程、IO等资源,造成其他级联服务所在服务器的崩溃
709 0
微服务怎么限流?算法+框架+实战!
|
消息中间件 中间件 Apache
二:apache的Qpid消息中间件介绍
一:什么是Qpid?--->Qpid 是 Apache 开发的一款面向对象的消息中间件,它是一个 AMQP 的实现,可以和其他符合 AMQP 协议的系统进行通信。--->Qpid 提供了 C++/Python/Java/C# 等主流编程语言的客户端库,安装使用非常方便。
1543 13