其实iOS的倒计时实现思路有多种,个人感觉难点在在于如何实现后台倒计时,保证数据的准确性。所以我们在研究GCD实现倒计时时,首先研究一下iOS关于时间的处理。
NSDate
NSDate是我们平时使用较多的一个类,先看下它的定义:
NSDate objects encapsulate a single point in time,
independent of any particular calendrical system or time zone.
Date objects are immutable,
representing an invariant time interval relative to an absolute reference date (00:00:00 UTC on 1 January 2001).
NSDate对象描述的是时间线上的一个绝对的值,和时区和文化无关,它参考的值是:以UTC为标准的,2001年一月一日00:00:00这一刻的时间绝对值。我们用编程语言描述时间的时候,都是以一个时间线上的绝对值为参考点,参考点再加上偏移量(以秒或者毫秒,微秒,纳秒为单位)来描述另外的时间点。
获取时间的API
NSDate* date = [NSDate date];
NSLog(@"current date interval: %f", [date timeIntervalSinceReferenceDate]);
timeIntervalSinceReferenceDate
返回的是距离参考时间的偏移量,这个偏移量的值为502945767秒,502945767/86400/365=15.9483056507,86400是一天所包含的秒数,365大致是一年的天数,15.94当然就是年数了,算出来刚好是此刻距离2001年的差值。
关于NSDate最重要的一点是:NSDate是受手机系统时间控制的。也就是说,当你修改了手机上的时间显示,NSDate获取当前时间的输出也会随之改变。在我们做App的时候,明白这一点,就知道NSDate并不可靠,因为用户可能会修改它的值。
CFAbsoluteTimeGetCurrent()
官方定义如下:
Absolute time is measured in seconds relative to the absolute reference date of Jan 1 2001 00:00:00 GMT.
A positive value represents a date after the reference date,
a negative value represents a date before it.
For example, the absolute time -32940326 is equivalent to December 16th, 1999 at 17:54:34.
Repeated calls to this function do not guarantee monotonically increasing results.
The system time may decrease due to synchronization with external time references or due to an explicit user change of the clock.
从上面的描述不难看出CFAbsoluteTimeGetCurrent()
的概念和NSDate非常相似,只不过参考点是:以GMT为标准的,2001年一月一日00:00:00这一刻的时间绝对值。同样CFAbsoluteTimeGetCurrent()
也会跟着当前设备的系统时间一起变化,也可能会被用户修改。
gettimeofday
这个API也能返回一个描述当前时间的值,代码如下:
struct timeval now;
struct timezone tz;
gettimeofday(&now, &tz);
NSLog(@"gettimeofday: %ld", now.tv_sec);
使用gettimeofday
获得的值是Unix time
。Unix time
又是什么呢?
Unix time
是以UTC 1970年1月1号 00:00:00为基准时间,当前时间距离基准点偏移的秒数。上述API返回的值是1481266031,表示当前时间距离UTC 1970年1月1号 00:00:00一共过了1481266031秒。
实际上NSDate也有一个API能返回Unix time:
NSDate* date = [NSDate date];
NSLog(@"timeIntervalSince1970: %f", [date timeIntervalSince1970]);
gettimeofday
和NSDate
,CFAbsoluteTimeGetCurrent()
一样,都是受当前设备的系统时间影响。只不过是参考的时间基准点不一样而已。我们和服务器通讯的时候一般使用Unix time。
mach_absolute_time()
mach_absolute_time()
可能用到的同学比较少,但这个概念非常重要。
前面提到我们需要找到一个均匀变化的属性值来描述时间,而在我们的iPhone上刚好有一个这样的值存在,就是CPU的时钟周期数(ticks)。这个tick
的数值可以用来描述时间,而mach_absolute_time()
返回的就是CPU已经运行的tick的数量。将这个tick数经过一定的转换就可以变成秒数,或者纳秒数,这样就和时间直接关联了。
不过这个tick数,在每次手机重启之后,会重新开始计数,而且iPhone锁屏进入休眠之后tick也会暂停计数。
mach_absolute_time()
不会受系统时间影响,只受设备重启和休眠行为影响。
CACurrentMediaTime()
CACurrentMediaTime()
可能接触到的同学会多一些,先看下官方定义:
/* Returns the current CoreAnimation absolute time. This is the result of
* calling mach_absolute_time () and converting the units to seconds.
*/
CFTimeInterval CACurrentMediaTime (void)
CACurrentMediaTime()
就是将上面mach_absolute_time()
的CPU tick数转化成秒数的结果。以下代码:
double mediaTime = CACurrentMediaTime();
NSLog(@"CACurrentMediaTime: %f", mediaTime);
返回的就是开机后设备一共运行了(设备休眠不统计在内)多少秒,另一个API也能返回相同的值:
NSTimeInterval systemUptime = [[NSProcessInfo processInfo] systemUptime];
NSLog(@"systemUptime: %f", systemUptime);
CACurrentMediaTime()
也不会受系统时间影响,只受设备重启和休眠行为影响。
sysctl
iOS系统还记录了上次设备重启的时间。可以通过如下API调用获取:
#include <sys/sysctl.h>
-(long)bootTime
{
int mib[2] = {CTL_KERN, KERN_BOOTTIME};
size_t size;
struct timeval boottime;
size = sizeof(boottime);
if (sysctl(mib, MIB_SIZE, &boottime, &size, NULL, 0) != -1)
{
return boottime.tv_sec;
}
return 0;
}
返回的值是上次设备重启的Unix time
。这个API返回的值也会受系统时间影响,用户如果修改时间,值也会随着变化。到此处,估计有的同学已经想到了方案,我们可以通过获取系统运行的时间,来获得进入后台的时间差。
//系统当前运行了多长时间
+(NSTimeInterval)uptimeSinceLastBoot
{
//获取当前设备时间时间戳 受用户修改时间影响
struct timeval now;
struct timezone tz;
gettimeofday(&now, &tz);
// NSLog(@"gettimeofday: %ld", now.tv_sec);
//获取系统上次重启的时间戳 受用户修改时间影响
struct timeval boottime;
int mib[2] = {CTL_KERN, KERN_BOOTTIME};
size_t size = sizeof(boottime);
double uptime = -1;
if (sysctl(mib, 2, &boottime, &size, NULL, 0) != -1 && boottime.tv_sec != 0)
{
//因为两个参数都会受用户修改时间的影响,因此它们想减的值是不变的
uptime = now.tv_sec - boottime.tv_sec;
uptime += (double)(now.tv_usec - boottime.tv_usec) / 1000000.0;
}
return uptime;
}
gettimeofday
和sysctl
都会受系统时间影响,但他们二者做一个减法所得的值,就和系统时间无关了。这样就可以避免用户修改时间影响我们。
后台倒计时问题
了解了我们的时间原理,接下来我们就我们就该研究如何实现后台休眠情况下倒计时问题了。
对于后台倒计时,网上有几种方案
- 通过系统提供的
- (void)applicationDidEnterBackground:(UIApplication *)application
方法,使应用程序不休眠,但是这个方法有审核风险(只要不是音乐 地图导航类的需要,苹果不允许后台执行) - 注册监听
applicationWillResignActive
和applicationDidBecomeActive
通知。自己记录后台休眠时间,然后当前倒计时时间与休眠时间相减。这里要注意最开始我们提到的时间问题,因为手机的时间用户是可以修改的,所以我们在处理时间问题上尽量使用PU的时钟周期数(ticks)。
具体实现我们可以这样:注意需要导入头文件#import <sys/time.h>
和#import <sys/sysctl.h>
//系统当前运行了多长时间
- (NSTimeInterval)uptimeSinceLastBoot {
//获取当前设备时间时间戳 受用户修改时间影响
struct timeval now;
struct timezone tz;
gettimeofday(&now, &tz);
// NSLog(@"gettimeofday: %ld", now.tv_sec);
//获取系统上次重启的时间戳 受用户修改时间影响
struct timeval boottime;
int mib[2] = {CTL_KERN, KERN_BOOTTIME};
size_t size = sizeof(boottime);
double uptime = -1;
if (sysctl(mib, 2, &boottime, &size, NULL, 0) != -1 && boottime.tv_sec != 0)
{
//因为两个参数都会受用户修改时间的影响,因此它们想减的值是不变的
uptime = now.tv_sec - boottime.tv_sec;
uptime += (double)(now.tv_usec - boottime.tv_usec) / 1000000.0;
}
return (NSInteger)uptime;
}
GCD倒计时具体实现Demo
#import "ViewController.h"
#import <sys/time.h>
#import <sys/sysctl.h>
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIButton *countDownBtn;
/** 系统时间 **/
@property (nonatomic, assign) NSTimeInterval systemUpTime;
/** 倒计时时间 **/
@property (nonatomic, assign) NSTimeInterval intervalTime;
/** 模拟暂停倒计时 **/
@property (nonatomic, assign) BOOL pause;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.intervalTime = 60;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self addNotification];
}
- (void)dealloc {
[self removeNotification];
}
- (IBAction)countDownBtnAction:(UIButton *)sender {
sender.userInteractionEnabled = false;
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(timer, dispatch_walltime(NULL, 0), 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
if (self.pause) return ;
self.intervalTime --;
NSLog(@"intervalTime:%f",self.intervalTime);
if (self.intervalTime <= 0) {
dispatch_source_cancel(timer);
dispatch_async(dispatch_get_main_queue(), ^{
[self.countDownBtn setTitle:@"获取验证码" forState:UIControlStateNormal];
self.countDownBtn.userInteractionEnabled = true;
self.intervalTime = 60;
[self.countDownBtn setTitleColor:[UIColor orangeColor] forState:UIControlStateNormal];
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self.countDownBtn setTitle:[NSString stringWithFormat:@"%ld秒后重新获取验证码",(long)self.intervalTime] forState:UIControlStateNormal];
[self.countDownBtn setTitleColor:[UIColor darkGrayColor] forState:UIControlStateNormal];
});
}
});
dispatch_resume(timer);
}
/**
* 添加通知
*/
- (void)addNotification
{
//监听是否触发home键挂起程序.
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:)
name:UIApplicationWillResignActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:)
name:UIApplicationDidBecomeActiveNotification object:nil];
}
/**
* 移除通知
*/
- (void)removeNotification
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
}
- (void)applicationWillResignActive:(NSNotification *)notification
{
self.systemUpTime = [self uptimeSinceLastBoot];
NSLog(@"即将进入后台:%f, ---%@",self.intervalTime,[NSDate date]);
self.pause = true;
}
- (void)applicationDidBecomeActive:(NSNotification *)notification
{
self.pause = false;
NSLog(@"即将进入前台:%f",self.intervalTime);
NSTimeInterval currentupTime = [self uptimeSinceLastBoot];
NSTimeInterval intervalTime = currentupTime - self.systemUpTime;
if (intervalTime > 0) {
NSLog(@"间隔 %f",intervalTime);
self.intervalTime -= intervalTime;
}
NSLog(@"继续倒计时:%f, ---%@",self.intervalTime, [NSDate date]);
}
//系统当前运行了多长时间
- (NSTimeInterval)uptimeSinceLastBoot {
//获取当前设备时间时间戳 受用户修改时间影响
struct timeval now;
struct timezone tz;
gettimeofday(&now, &tz);
// NSLog(@"gettimeofday: %ld", now.tv_sec);
//获取系统上次重启的时间戳 受用户修改时间影响
struct timeval boottime;
int mib[2] = {CTL_KERN, KERN_BOOTTIME};
size_t size = sizeof(boottime);
double uptime = -1;
if (sysctl(mib, 2, &boottime, &size, NULL, 0) != -1 && boottime.tv_sec != 0)
{
//因为两个参数都会受用户修改时间的影响,因此它们想减的值是不变的
uptime = now.tv_sec - boottime.tv_sec;
uptime += (double)(now.tv_usec - boottime.tv_usec) / 1000000.0;
}
return (NSInteger)uptime;
}
@end
网友评论