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
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。