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

目录
相关文章
|
存储 API
BackTrader 中文文档(二十九)(4)
BackTrader 中文文档(二十九)
284 0
|
Dart 前端开发 安全
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践
【4月更文挑战第30天】本文探讨了Flutter中线程管理和并发编程的关键性,强调其对应用性能和用户体验的影响。Dart语言提供了`async`、`await`、`Stream`和`Future`等原生异步支持。Flutter采用事件驱动的单线程模型,通过`Isolate`实现线程隔离。实践中,可利用`async/await`、`StreamBuilder`和`Isolate`处理异步任务,同时注意线程安全和性能调优。参考文献包括Dart异步编程、Flutter线程模型和DevTools文档。
604 1
【Flutter前端技术开发专栏】Flutter中的线程与并发编程实践
flutter 引用图片资源遇到的问题
flutter 引用图片资源遇到的问题
289 1
|
JavaScript Java 测试技术
基于springboot+vue.js+uniapp的华为数码商城交易平台附带文章源码部署视频讲解等
基于springboot+vue.js+uniapp的华为数码商城交易平台附带文章源码部署视频讲解等
153 2
|
存储 Java 编译器
JNI内存方面说明以及相关类型手动释放内存
JNI内存方面说明以及相关类型手动释放内存
1466 0
|
存储 图形学
【推荐100个unity插件之13】推荐一款开源的Unity网格破碎插件,实现在Unity中展示可破坏的墙壁的——unity-fracture
【推荐100个unity插件之13】推荐一款开源的Unity网格破碎插件,实现在Unity中展示可破坏的墙壁的——unity-fracture
383 0
|
算法 Serverless C语言
CMake函数和宏(function和macro):使用函数和宏提高代码可读性
CMake函数和宏(function和macro):使用函数和宏提高代码可读性
357 1
|
传感器 安全 编译器
【C++断言机制】深入理解C/C++ 中静态断言static_assert与断言 assert
【C++断言机制】深入理解C/C++ 中静态断言static_assert与断言 assert
474 0
|
存储 安全 C++
【C++ 泛型编程 高级篇】C++可变参数模板探索:编程技巧与实战应用
【C++ 泛型编程 高级篇】C++可变参数模板探索:编程技巧与实战应用
743 0
|
JavaScript Linux Windows
Nodejs升级到指定版本
Nodejs升级到指定版本
1314 0