- /*!
- * @structure LXDReceiver
- * 存储回调结构体
- *
- * @var lefttime 倒计时时长
- * @var objaddr 保留的对象地址
- * @var callback 回调block
- */
- typedef struct LXDReceiver {
- long lefttime;
- uintptr_t objaddr;
- LXDReceiverCallback callback;
- }
- LXDReceiver;
同样将定时器管理设计成单例使用,由于回调对象可能在倒计时完成之前就结束倒计时任务,借鉴 enum 的停止机制,回调传入一个 BOOL * 变量,允许设置为 YES,提前结束任务
- /*!
- * @block LXDTimerCallback
- * 回调block
- *
- * @params leftTime 倒计时剩余秒数
- * @params isStop 等同于enum的isStop,修改为YES后定时任务结束
- */
- typedef void(^LXDTimerCallback)(long leftTime, bool *isStop);
- /*!
- * @class LXDTimerManager
- * 定时器管理
- */
- @interface LXDTimerManager : NSObject
- /*!
- * @method timerManager
- * 获取定时器管理对象
- */
- + (instancetype)timerManager;
- /*!
- * @method registerCountDown:forSeconds:withReceiver:
- * 注册倒计时回调
- *
- * @params countDown 回调block
- * @params seconds 倒计时长
- * @params receiver 注册的对象
- */
- - (void)registerCountDown: (LXDTimerCallback)countDown
- forSeconds: (NSUInteger)seconds
- withReceiver: (id)receiver;
- @end
在倒计时回调中,遍历所有的 receivers,一旦任务的倒计时结束或者标记位被修改,那么从存储结构中剔除注册者:
- - (void) _countDown {
- unsigned int receiversCount = 0;
- for (unsigned int offset = 0; offset < _receives - >entries_count; offset++) {
- hash_entry_t * entry = _receives - >hash_entries + offset;
- LXDReceiverNode * header = (LXDReceiverNode * ) entry - >entry;
- __block LXDReceiverNode * node = header - >next;
- dispatch_async(dispatch_get_main_queue(), ^{
- lxd_wait(self.lock);
- while (node != NULL) {
- LXDReceiver * receiver = node - >receiver;
- LXDReceiverNode * next = node - >next;
- receiver - >lefttime--;
- bool isStop = false;
- receiver - >callback(receiver - >lefttime, &isStop);
- if (receiver - >lefttime <= 0 || isStop) {
- _receives - >destoryNode(node);
- header - >count--;
- }
- node = next;
- }
- lxd_signal(self.lock);
- });
- receiversCount += header - >count;
- }
- if (receiversCount == 0 && self.timer != nil) {
- lxd_wait(self.lock);
- dispatch_cancel(self.timer);
- self.timer = nil;
- lxd_signal(self.lock);
- }
- }
假如本次回调中正好所有的倒计时任务都处理完毕了,所有的注册者都被清除。也不会立刻停止定时器,而是等待到下次回调再停止。主要出于两个条件考虑:
按钮的倒计时是一种特殊的需求,一般在倒计时前后不会出现相同的二次任务。但是在商品抢购的需求中,同一个 cell 注册多次倒计时任务非常的常见,因此还要考虑如何设计去重。正好 @synchronized 有类似的去重设计可以借鉴,采用
的设计。由于同一个对象在生命周期内地址是不变的,因此以地址进行 hash 运算:
- hashmap + linked list
- unsigned int LXDBaseHashmap: :obj_hash_code(void * obj) {
- uint64_t * val1 = (uint64_t * ) obj;
- uint64_t * val2 = val1 + 1;
- return (unsigned int)( * val1 + *val2) % hash_bucket_count;
- }
- bool LXDReceiverHashmap: :insertReceiver(void * obj, LXDReceiverCallback callback, unsigned long lefttime) {
- unsigned int offset = obj_hash_code(obj);
- hash_entry_t * entry = hash_entries + offset;
- LXDReceiverNode * header = (LXDReceiverNode * ) entry - >entry;
- LXDReceiverNode * node = header - >next;
- if (node == NULL) {
- LXDReceiver * receiver = create_receiver(obj, callback, lefttime);
- node = new LXDReceiverNode(receiver, header);
- header - >next = node;
- header - >count++;
- return true;
- }
- do {
- if (compare(node, obj) == true) {
- node - >receiver - >callback = callback;
- node - >receiver - >lefttime = lefttime;
- return false;
- }
- } while ( node - > next != NULL && ( node = node - > next ));
- if (compare(node, obj) == true) {
- node - >receiver - >callback = callback;
- node - >receiver - >lefttime = lefttime;
- return false;
- }
- LXDReceiver * receiver = create_receiver(obj, callback, lefttime);
- node - >next = new LXDReceiverNode(receiver, node);
- header - >count++;
- return true;
- }
当匹配到同一个对象存在倒计时任务时,只需要更新任务的剩余时间和回调处理即可
应对前后台切换导致的时间错乱问题,上面我提出了三种方案。除了第
种方案毫无意义之外,需要在前两种做选择。由于我使用的数据结构中存储的是 lefttime 而不是 deadline,使用第 2 中计算时间差值是最合适的做法:
- 3
- - (void) applicationDidBecameActive: (NSNotification * ) notif {
- if (self.enterBackgroundTime && self.timer) {
- long delay = [[NSDate date] timeIntervalSinceDate: self.enterBackgroundTime];
- dispatch_suspend(self.timer);
- for (unsigned int offset = 0; offset < _receives - >entries_count; offset++) {
- hash_entry_t * entry = _receives - >hash_entries + offset;
- LXDReceiverNode * header = (LXDReceiverNode * ) entry - >entry;
- __block LXDReceiverNode * node = header - >next;
- dispatch_async(dispatch_get_main_queue(), ^{
- lxd_wait(self.lock);
- while (node != NULL) {
- LXDReceiver * receiver = node - >receiver;
- LXDReceiverNode * next = node - >next;
- receiver - >lefttime -= delay;
- bool isStop = false;
- receiver - >callback(receiver - >lefttime < 0 ? 0 : receiver - >lefttime, &isStop);
- if (receiver - >lefttime <= 0 || isStop) {
- _receives - >destoryNode(node);
- header - >count--;
- }
- node = next;
- }
- lxd_signal(self.lock);
- });
- }
- dispatch_resume(self.timer);
- }
- } - (void) applicationDidEnterBackground: (NSNotification * ) notif {
- self.enterBackgroundTime = [NSDate date];
- }
由于通知的回调线程和定时器的处理线程可能存在多线程的竞争,为了排除这一干扰,我采用了 sema 加锁,以及在遍历期间挂起定时器,减少不必要的麻烦
首先,block 的循环引用是很容易去避免的一个问题,但为了减少书写__weak 的麻烦,可以在接口设计上去避免这个问题。由于这套倒计时方案并不局限于 UIButton 这个类,基于 NSObject 进行方法的扩展是最简单有效的方法:
- @interface NSObject (PerformTimer)
- /*!
- * @method beginCountDown:forSeconds:
- * 启动倒计时任务
- *
- * @params countDown 倒计时回调block
- * @params seconds 倒计时长
- */
- - (void)beginCountDown: (LXDObjectCountDown)countDown forSeconds: (NSInteger)seconds;
- @end
- @implementation NSObject (PerformTimer)
- - (void)beginCountDown: (LXDObjectCountDown)countDown forSeconds: (NSInteger)seconds {
- if (countDown == nil || seconds <= 0) { return; }
- __weak typeof(self) weakself = self;
- [[LXDTimerManager timerManager] registerCountDown: ^(long leftTime, bool *isStop) {
- countDown(weakself, leftTime, (BOOL *)isStop);
- } forSeconds: seconds withReceiver: self];
- }
- @end
还是要再次声明这个观点:
倒计时方案几乎没有门槛,但也不仅限于倒计时方案
设计一个功能需要经过仔细考虑多个因素,包括逻辑、性能、质量多个方面。本篇文章基于这段时间学习的收获总结而成,如果您觉得有不足之处,还万请指出。项目已同步至 cocoapods,可通过
导入 demo
- pod 'LXDTimerManager'
来源: https://juejin.im/post/5a50657d518825733060be8b