问题:如何确保由于运行循环事件(计时器,用户交互,performSelector等)而执行的代码具有“现在”的相同概念?
背景:假设事件处理程序需要100毫秒才能执行,这意味着[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被隐藏了,因为调用之间“ now”的变化很小。
这是一个非常简化的示例,其中通过简单地调用[NSDate date]并将该实例发送给所有调用者就可以轻松解决此问题。这是我感兴趣的一般情况,尽管调用链可能非常深,周期性,可重入等。
NSRunLoop
是的包装器CFRunLoop
。CFRunLoop
具有NSRunLoop
未公开的功能,因此有时您必须降至CF级别。
这样的功能之一就是观察者,它是您可以注册的回调,当运行循环进入不同的阶段时可以调用该回调。在这种情况下,您想要的阶段是一个等待后的观察者,它在运行循环接收到事件(从源,由于计时器触发或由于将块添加到主队列)而收到事件后调用。
让我们添加一个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
。
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句