【问题标题】:Timestamp / NSDate for current event start in UIKitUIKit 中当前事件开始的时间戳/NSDate
【发布时间】:2018-03-12 15:34:01
【问题描述】:

问题: 如何确保由于 runloop 事件(计时器、用户交互、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,因为“现在”在两次调用之间发生了非常小的变化。

这是一个非常简化的示例,只需调用 [NSDate date] 并将该实例发送给所有调用者即可轻松修复。这是我感兴趣的一般情况,尽管调用链可能非常深、循环、可重入等。

【问题讨论】:

  • 在您的问题中包含一些相关代码。

标签: ios uikit nsdate nsrunloop


【解决方案1】:

NSRunLoopCFRunLoop 的包装器。 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

run loop 可以在不同的模式下运行,虽然有少量的常见模式,但理论上可以动态创建新模式。如果您希望在特定模式下调用观察者,则必须为该模式注册它。因此,为了确保报告的日期始终正确,我们不仅要记住日期,还要记住我们记录日期的方式:

@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

【讨论】:

  • 回答的很详细,非常感谢!不过有个问题,您在创建处理程序时为“订单”传递了“-2000000”。文档说:“除非有理由不这样做,否则通过 0。”为什么是-2000000?
  • 因为我们希望观察者在运行循环唤醒后很早就运行,在其他任何东西运行之前。
猜你喜欢
  • 2014-04-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多