为 RAC 定时器提供一个可独立区分延迟执行和间隔执行时间的定时器扩展
背景
这几天有一个需求需要 Native 侧来承接,一句话描述就是 APP 应用前台运行时间以心跳的方式记录并上报。需求很简单,也是非常小的功能,基本上一个定时器加上前后台状态监控即可完成。
最初,我用了很短的时间基于 GCD 定时器搭建完成了这套心跳上报流程:
- 应用启动后立刻启动定时任务并开始心跳连接上报
- 根据指定间隔执行时间循环执行心跳连接
- 应用退入后台(包括切换应用、锁屏等操作)时,停止当前定时任务,断开心跳连接
- 应用返回前台时,重启定时任务,重新进行心跳连接
核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| static NSTimeInterval const kDelayTime = 0.f; static NSTimeInterval const kTimeInterval = 60.f;
@interface YCHeartbeatManager()
@property (nonatomic) dispatch_source_t timer;
@end
@implementation YCHeartbeatManager
+ (instancetype)sharedInstance { static YCHeartbeatManager *shareInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ shareInstance = [[YCHeartbeatManager alloc] init]; });
return shareInstance; }
- (instancetype)init { self = [super init]; if (self) { [self registerRACObserver]; } return self; }
- (void)startHearbeatTask { self.timer = [YCThreadTool gcd_timer]; @weakify(self) [YCThreadTool gcd_dispatchTimer:self.timer delayTime:kDelayTime timeInterval:kTimeInterval handler:^{ @strongify(self) [self heartbeatLink]; }]; }
#pragma mark - Private
- (void)stopHearbeatTask { [YCThreadTool gcd_dispatchCancelTask:self.timer]; self.timer = nil; }
- (void)restartHearbeatTask { [self startHearbeatTask]; }
- (void)registerRACObserver { @weakify (self); [[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIApplicationWillEnterForegroundNotification object:nil] takeUntil:self.rac_willDeallocSignal] subscribeNext:^(id x) { @strongify (self); [self restartHearbeatTask]; }]; [[[[NSNotificationCenter defaultCenter] rac_addObserverForName:UIApplicationDidEnterBackgroundNotification object:nil] takeUntil:self.rac_willDeallocSignal] subscribeNext:^(id x) { @strongify (self); [self stopHearbeatTask]; }]; }
- (void)heartbeatLink { [[self heartbeatLinkSignal] subscribeNext:^(id x) { NSLog(@"心跳连接完成"); }]; }
- (RACSignal *)heartbeatLinkSignal { return [[[YCHeartbeatReportCMD alloc] init].rac_start flattenMap:^RACStream *(YCMathRequest *request) { return [RACSignal return:request.responseJSONObject]; }]; }
@end
|
总共不到100行代码,高效的解决了这个需求,看上去一切都很完美。
RAC 定时器的问题
晚些时候,同事在 review 的时候看到了这个类,反馈希望可以统一使用 RAC 的定时器来完成这个需求,工程内希望可以统一技术栈。
既然如此,那我们就开始重构,把 GCD 的定时器替换成 RAC 的定时器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| ... @property (nonatomic, strong) RACDisposable *disposable;
...
- (void)startHearbeatTask { @weakify(self) self.disposable = [[RACSignal interval:kTimeInterval onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) { @strongify(self) [self heartbeatLink]; }]; }
- (void)stopHearbeatTask { [self.disposable dispose]; self.disposable = nil; }
- (void)restartHearbeatTask { [self startHearbeatTask]; } ...
|
看上去没什么问题,不过是定时器的替换而已,于是在重构完定时器部分后,我开始运行工程检测定时器的效果。
这个时候问题出现了:
RAC 定时器的首次执行时间比首次启动时间晚了整整1分钟,而这1分钟刚好是我设定的 interval
的值,这就是个大问题了:定时器的延迟执行时间和执行间隔时间挂钩了。
至少从运行表现上看,这两个时间是挂钩的。但我想 Reactive 这种级别的框架应该不会犯这种低级错误,或者不考虑两者可能独立共存的场景,于是我仔细查看了 RAC 定时器的源码:
RACSignal+Operations
RACQueueScheduler
看完后只想说一句话:WTF???
RAC 的定时器底层还是一套 GCD 的定时器,相当于给 GCD 定时器做了套兼容 RACSignal 的封装。这本来没什么,但是要命的是,RAC 的作者将延迟执行时间与执行间隔时间这两个参数进行了强关联,这两个还真的是挂钩的,且外部不可对延迟执行时间做任何操作。这也就解释了为什么在我设置了1分钟的执行间隔时间后,我的定时器会在启动后延迟1分钟的时间才开始执行。
为 RAC 定时器提供扩展
面对着这样一个要命的问题该怎么办呢?本着还是不相信 RAC 的贡献者会这么”菜”的原则,我和同事翻阅了所有 RAC 定时器可提供的 API,以及所有可能另 RACDisposable 强制立即执行的函数,最后一无所获,它真的就是互相挂钩且不支持单独设置延迟启动时间的😢。
在一阵绝望之后,我似乎想到了什么,于是乎我去搜索了工程中所有的 RAC 定时器使用场景,发现无一例外都是倒计时的定时应用。这似乎可以解释 RAC 为什么如此设计了,它可能最初在设计的时候就是为倒计时定时准备的。
它本就不适合出现在我们当前需求的这个场景里,因为它不是为此设计的。
这样一想一切就都说的通了,虽然可以说通了,但是问题依旧没有解决。目前可以确定的是,RAC 没有为后台定时场景提供任何一个 API,如果依旧想要用 RAC 来进行后台定时的话,就需要自己动手为 RAC 添加扩展,自己来实现一套支持独立区分延迟执行和间隔执行时间的定时器了。
在参考了现有的 RAC 定时器源码后,我在主工程中加入了两个 Category,分别是底层控制 GCD 定时器的 RACQueueScheduler+YCTimer 和 提供定时器上层抽象的 RACSignal+YCOperations
RACQueueScheduler+YCTimer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #import "RACQueueScheduler.h"
NS_ASSUME_NONNULL_BEGIN
@interface RACQueueScheduler (YCTimer)
- (RACDisposable *)afterDelayTime:(NSTimeInterval)delayTime repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block;
@end
NS_ASSUME_NONNULL_END
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #import "RACQueueScheduler+YCTimer.h"
@implementation RACQueueScheduler (YCTimer)
- (RACDisposable *)afterDelayTime:(NSTimeInterval)delayTime repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block { NSCParameterAssert(interval > 0.0 && interval < INT64_MAX / NSEC_PER_SEC); NSCParameterAssert(leeway >= 0.0 && leeway < INT64_MAX / NSEC_PER_SEC); NSCParameterAssert(block != NULL);
uint64_t intervalInNanoSecs = (uint64_t)(interval * NSEC_PER_SEC); uint64_t leewayInNanoSecs = (uint64_t)(leeway * NSEC_PER_SEC);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue); dispatch_source_set_timer(timer, delayTime, intervalInNanoSecs, leewayInNanoSecs); dispatch_source_set_event_handler(timer, block); dispatch_resume(timer);
return [RACDisposable disposableWithBlock:^{ dispatch_source_cancel(timer); }]; }
@end
|
RACSignal+YCOperations
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #import "RACSignal.h"
NS_ASSUME_NONNULL_BEGIN
@interface RACSignal (YCOperations)
+ (RACSignal *)delayTime:(NSTimeInterval)delayTime interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler;
+ (RACSignal *)delayTime:(NSTimeInterval)delayTime interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler withLeeway:(NSTimeInterval)leeway;
@end
NS_ASSUME_NONNULL_END
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| #import "RACSignal+YCOperations.h" #import "RACQueueScheduler+YCTimer.h"
@implementation RACSignal (YCOperations)
+ (RACSignal *)delayTime:(NSTimeInterval)delayTime interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler { return [[RACSignal delayTime:delayTime interval:interval onScheduler:scheduler withLeeway:0.0] setNameWithFormat:@"+interval: %f onScheduler: %@", (double)interval, scheduler]; }
+ (RACSignal *)delayTime:(NSTimeInterval)delayTime interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler withLeeway:(NSTimeInterval)leeway { NSCParameterAssert(scheduler != nil); NSCParameterAssert(scheduler != RACScheduler.immediateScheduler);
return [[RACSignal createSignal:^(id<RACSubscriber> subscriber) { return [(RACQueueScheduler *)scheduler afterDelayTime:delayTime repeatingEvery:interval withLeeway:leeway schedule:^{ [subscriber sendNext:[NSDate date]]; }]; }] setNameWithFormat:@"+interval: %f onScheduler: %@ withLeeway: %f", (double)interval, scheduler, (double)leeway]; }
@end
|
RAC 定时器扩展的使用
在使用方面与 RAC 本身的定时器 API 几乎完全相同,可以做到毫无替换感知:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| static NSTimeInterval const kDelayTime = 0.f; static NSTimeInterval const kTimeInterval = 60.f;
...
- (void)startHearbeatTask { @weakify(self) self.disposable = [[RACSignal interval:kTimeInterval onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) { @strongify(self) [self heartbeatLink]; }]; }
- (void)startHearbeatTask { @weakify(self) self.disposable = [[RACSignal delayTime:kDelayTime interval:kTimeInterval onScheduler:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDate * _Nullable x) { @strongify(self) [self heartbeatLink]; }]; }
...
|
运行后一切正常,完美的替换,现在终于可以独立控制执行延迟时间和执行间隔时间了,因为本质上仍然是 GCD 定时器,所以不用担心性能和定时器生命周期的维护等任何问题。
总结
估计是提供给 Objective-C 使用的 RAC 版本年老失修无人维护了,导致这种使用场景未能完成覆盖,实在是可惜,不过好在我们自己动手添加了一套扩展完成这部分内容的补充。
最后想说点什么呢,赶紧换 Swift 吧,RxSwift 里什么都有 [手动狗头]。