问题: 如何确保由于runloop事件(计时器,用户交互,performSelector等)执行的代码具有相同的概念"现在"?
背景 假设事件处理程序需要100ms才能执行,这意味着[NSDate date]将返回稍微不同的"现在"取决于您在执行中何时拨打电话。如果你对时机非常不走运,你甚至可能会在两次通话之间有不同的日期。
这会因依赖当前时间进行各种计算而产生问题,因为这些计算在执行过程中可能会有所不同。
当然,对于特定的事件处理程序,您只需将日期存储在AppDelegate或类似事件中,或者在从入口点开始的每次调用中将其传递。
然而,我想要一些更安全和自动的东西。理想情况下,我想知道当前运行循环何时开始处理事件。我可以简单地用[NSDate date]代替,并且总是得到相同的结果,直到下一个事件被触发为止。
我没有太多运气就查看了NSRunLoop的文档。我还研究了CADisplayLink的潜在解决方法。两者都没有提供明确的答案。
感觉这应该是一件普通的事情,而不是那些需要"变通办法的事情。我的猜测是,我正在寻找错误的地方或使用错误的搜索字词。
代码示例:
UIView *_foo, _fie;
NSDate *_hideDate;
- (void)handleTimer
{
[self checkVisible:_foo];
[self checkVisible:_fie];
}
- (void)checkVisible:(UIView *)view
{
view.hidden = [_hideDate timeIntervalSinceNow] < 0];
}
在这种情况下,当_foo仍然可见时,我们最终会隐藏_fie,因为&#34;现在&#34;在通话之间变化很小。
这是一个非常简化的示例,通过简单地调用[NSDate date]并将该实例发送给所有调用者,修复很简单。我感兴趣的是一般情况,尽管调用链可能非常深,循环,可重入等等。
答案 0 :(得分:2)
NSRunLoop
是CFRunLoop
的包装器。 CFRunLoop
具有NSRunLoop
未公开的功能,因此有时您必须降至CF级别。
一个这样的功能是观察者,它们是你可以注册的回调,当运行循环进入不同的阶段时被调用。在这种情况下,您想要的阶段是等待后观察者,在运行循环接收事件(来自源,或由于计时器触发,或由于块被添加到主队列)之后调用。 / p>
我们将wakeDate
属性添加到NSRunLoop
:
// NSRunLoop+wakeDate.h
#import <Foundation/Foundation.h>
@interface NSRunLoop (wakeDate)
@property (nonatomic, strong, readonly) NSDate *wakeDate;
@end
使用此类别,我们可以随时向NSRunLoop
询问wakeDate
属性,例如:
#import "AppDelegate.h"
#import "NSRunLoop+wakeDate.h"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5 repeats:YES block:^(NSTimer *timer){
NSLog(@"timer: %.6f", NSRunLoop.currentRunLoop.wakeDate.timeIntervalSinceReferenceDate);
}];
[NSRunLoop.currentRunLoop addTimer:timer forMode:NSRunLoopCommonModes];
return YES;
}
@end
要实现此属性,我们将创建一个WakeDateRecord
类,我们可以将它作为关联对象附加到运行循环:
// NSRunLoop+wakeDate.m
#import "NSRunLoop+wakeDate.h"
#import <objc/runtime.h>
@interface WakeDateRecord: NSObject
@property (nonatomic, strong) NSDate *date;
- (instancetype)initWithRunLoop:(NSRunLoop *)runLoop;
@end
static const void *wakeDateRecordKey = &wakeDateRecordKey;
@implementation NSRunLoop (wakeDate)
- (NSDate *)wakeDate {
WakeDateRecord *record = objc_getAssociatedObject(self, wakeDateRecordKey);
if (record == nil) {
record = [[WakeDateRecord alloc] initWithRunLoop:self];
objc_setAssociatedObject(self, wakeDateRecordKey, record, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return record.date;
}
@end
运行循环可以在不同的模式下运行,虽然存在少量的共模,但理论上可以动态创建新模式。如果希望在特定模式下调用观察者,则必须为该模式注册它。因此,为了确保报告的日期始终正确,我们不仅会记住日期,还会记住我们记录日期的模式:
@implementation WakeDateRecord {
NSRunLoop *_runLoop;
NSRunLoopMode _dateMode;
NSDate *_date;
CFRunLoopObserverRef _observer;
}
要初始化,我们只存储运行循环并创建观察者:
- (instancetype)initWithRunLoop:(NSRunLoop *)runLoop {
if (self = [super init]) {
_runLoop = runLoop;
_observer = CFRunLoopObserverCreateWithHandler(nil, kCFRunLoopEntry | kCFRunLoopAfterWaiting, true, -2000000, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
[self setDate];
});
}
return self;
}
当询问日期时,我们首先检查当前模式是否与我们记录模式的日期不同。如果是这样,那么当运行循环在当前模式中醒来时,日期不会更新。这意味着观察者没有注册当前模式,所以我们现在应该注册并立即更新日期:
- (NSDate *)date {
NSRunLoopMode mode = _runLoop.currentMode;
if (![_dateMode isEqualToString:mode]) {
// My observer didn't run when the run loop awoke in this mode, so it must not be registered in this mode yet.
NSLog(@"debug: WakeDateRecord registering in mode %@", mode);
CFRunLoopAddObserver(_runLoop.getCFRunLoop, _observer, (__bridge CFRunLoopMode)mode);
[self setDate];
}
return _date;
}
当我们更新日期时,我们还需要更新存储模式:
- (void)setDate {
_date = [NSDate date];
_dateMode = _runLoop.currentMode;
}
@end
关于此解决方案的重要警告:观察者每次通过运行循环时触发一次。运行循环可以在单次传递期间为多个定时器和多个块添加到主队列提供服务。所有服务的定时器或块都会看到相同的wakeDate
。