我们经常需要在应用中使用倒计时的功能,常见地方有发送验证码、限时抢购等等。通常我们可以使用 NSTimer、GCD 等方式来进行计时操作,但是当应用进入后台时,倒计时便会停止,再回来后计时就不准确了,那如何让倒计时尽量的准确呢,这里提供了几个方案可以作为参考。
# 在后台运行倒计时
倒计时在应用进入后台后停止是因为进入后台后系统会暂停当前应用的运行,那么我们便可以通过 beginBackgroundTaskWithExpirationHandler
来向系统申请后台任务,这样定时器就可以在后台正常运行。
申请后台任务的方法如下,在 AppDelegate
中添加下面的方法
@interface AppDelegate ()
@property (nonatomic, assign) UIBackgroundTaskIdentifier backTaskID;
@end
@implementation AppDelegate
- (void)backTaskHandle{
__weak __typeof__ (self) weakSelf = self;
self.backTaskID = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (weakSelf) strongSelf = weakSelf;
[[UIApplication sharedApplication] endBackgroundTask:strongSelf.backTaskID];
strongSelf.backTaskID = UIBackgroundTaskInvalid;
[[UIApplication sharedApplication] clearKeepAliveTimeout];
}];
}
- (void)applicationDidEnterBackground:(UIApplication *)application {
BOOL backgroundAccepted = [[UIApplication sharedApplication] setKeepAliveTimeout:600 handler:^{
[self backTaskHandle];
}];
[self backTaskHandle];
}
@end
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 记录应用进入后台的时间
虽然在后台运行倒计时可以解决问题,但是后台任务也有可能会失败,另外审核的时候也增加了不可靠的因素。如果我们可以在进入后台时记录时间,然后当应用回到前台时也获得时间,这样通过时间差就可以得知应用在后台的时间,通过调节定时器的剩余时间也可以做到正确的计时。不过如果单纯的记录系统时间当用户在后台手动修改系统时间后计时就不再准确了,那有没有什么办法可以不受用户修改时间的影响呢?这里就需要下面两个系统方法了。
使用 gettimeofday()
可以获得 Unix time, 就是以 UTC 1970 年 1 月 1 号 00:00:00 为基准时间,当前时间距离基准点偏移的秒数。这个时间会受系统时间影响。
使用 sysctl()
可以获得上次设备重启的 Unix time。这个值也会受系统时间影响,用户如果修改时间,值也会随着变化。
通过上面两个方法分别获得当前 Unix time 与设备重启时的 Unix time,他们的差就是当前设备距离上次重启一共过了多少时间,由于两个时间都会受系统时间影响,也就是他们的差是不会变的,这样我们就可以得到一个与系统时间无关的数值。在应用进入后台时记录一次设备运行时间,应用进入前台时再获得一次设备运行时间,这样两次运行时间的差就是设备在后台运行的时间,这个时间不会因为用户修改系统时间而改变。
#include <sys/sysctl.h>
//获取系统当前运行了多长时间
- (NSTimeInterval)deviceRunTime{
//获取当前设备时间时间戳 受用户修改时间影响
struct timeval now;
struct timezone tz;
gettimeofday(&now, &tz);
//获取系统上次重启的时间戳 受用户修改时间影响
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
添加监听进入后台与前台的通知计算在后台的运行时间
- (void)addNotification{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:)
name:UIApplicationWillResignActiveNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:)
name:UIApplicationDidBecomeActiveNotification object:nil];
}
- (void)applicationWillResignActive:(NSNotification *)notification{
// 记录当前系统的运行时间
self.systemTime = [self deviceRunTime];
}
- (void)applicationDidBecomeActive:(NSNotification *)notification{
// 获取当前系统的运行时间
NSTimeInterval currentTime = [self deviceRunTime];
// 获取时间差
NSTimeInterval intervalTime = currentTime - self.systemTime;
// 获取系统在后台运行的时间
if (intervalTime > 0) {
self.intervalTime = intervalTime;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 倒计时的设计
除了要注意定时器在应用进入后台后保持正确的计时外,如何设计应用内的倒计时方案也是很重要的,这里提供几种方案来参考。
# 每个计时控件单独计时
这个无疑是最简单的方案,为每个需要计时的地方单独开启定时器,配合定时器在后台的运行方案可以实现比较准确的倒计时。但是如果页面倒计时的地方太多,尤其是当带有定时器的 cell 需要复用时这样就不好进行管理,还容易造成内存泄露的问题。
# 设计管理类统一计时
我们可以单独设计一个专门用来定时的管理类,这个类维护一个定时器,跟一个任务列表 (可以使用 NSHashTable
来实现)。这样当需要定时任务时可以调用方法将定时任务注册到列表中去,完成的定时任务及时从列表中移除,如果当前任务列表为空则可以停止定时器,当下次有定时任务时再开启。这样就可以方便的管理所有的定时任务。