从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的定时器。

目录
相关文章
|
Python
探索LightGBM:异常值处理与鲁棒建模
探索LightGBM:异常值处理与鲁棒建模【2月更文挑战第2天】
453 0
|
存储 API
BackTrader 中文文档(二十九)(4)
BackTrader 中文文档(二十九)
235 0
flutter 引用图片资源遇到的问题
flutter 引用图片资源遇到的问题
257 1
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的华为数码商城交易平台附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的华为数码商城交易平台附带文章源码部署视频讲解等
138 2
|
存储 图形学
【推荐100个unity插件之13】推荐一款开源的Unity网格破碎插件,实现在Unity中展示可破坏的墙壁的——unity-fracture
【推荐100个unity插件之13】推荐一款开源的Unity网格破碎插件,实现在Unity中展示可破坏的墙壁的——unity-fracture
343 0
|
存储 Java 编译器
JNI内存方面说明以及相关类型手动释放内存
JNI内存方面说明以及相关类型手动释放内存
1353 0
|
算法 Serverless C语言
CMake函数和宏(function和macro):使用函数和宏提高代码可读性
CMake函数和宏(function和macro):使用函数和宏提高代码可读性
293 1
|
传感器 安全 编译器
【C++断言机制】深入理解C/C++ 中静态断言static_assert与断言 assert
【C++断言机制】深入理解C/C++ 中静态断言static_assert与断言 assert
407 0
|
JavaScript Linux Windows
Nodejs升级到指定版本
Nodejs升级到指定版本
1097 0
|
消息中间件 中间件 Apache
二:apache的Qpid消息中间件介绍
一:什么是Qpid?--->Qpid 是 Apache 开发的一款面向对象的消息中间件,它是一个 AMQP 的实现,可以和其他符合 AMQP 协议的系统进行通信。--->Qpid 提供了 C++/Python/Java/C# 等主流编程语言的客户端库,安装使用非常方便。
1645 86