您可以通过实现节能api而不是计时器来减少应用程序的能耗。例如,NSURLSession API提供了执行进程外后台URL会话和在会话完成时接收通知的能力。请参见延迟联网。如果你必须使用计时器,就要有效地使用它们。
计时器的高成本
计时器允许您计划延迟或周期性操作。计时器等待到某个时间间隔结束,然后触发,执行特定操作,例如向其目标对象发送消息。当CPU和其他系统从低功耗、空闲状态唤醒时,将系统从空闲状态唤醒会产生能量消耗。如果计时器导致系统唤醒,则会产生该成本。
应用程序经常不必要地使用计时器。如果你在你的应用程序中使用计时器,考虑你是否真的需要它们。例如,有些应用程序在应该响应事件时使用计时器来轮询状态更改。其他应用程序在应该使用信号量或其他锁以获得最大效率时使用计时器作为同步工具。有些计时器在执行时没有适当的超时,导致它们在不再需要时继续触发。不管是哪种情况,如果有许多计时器调用的唤醒,则能量影响很大
不使用计时器获取事件通知
有些应用程序使用计时器监视文件内容、网络可用性和其他状态更改的更改。定时器防止CPU进入或停留在空闲状态,这会增加能耗并消耗电池电量。
与其使用计时器来监视事件,不如使用更高效的服务,例如调度源。见清单5-1。
清单5-1建议:使用节能的调度源获取文件更改通知
const char *myFile = [@"/Path/To/File" fileSystemRepresentation];
int fileDescriptor = open(myFile, O_EVTONLY);
dispatch_queue_t myQueue = dispatch_get_main_queue();
const uint64_t dispatchFlags = DISPATCH_VNODE_DELETE | DISPATCH_VNODE_WRITE;
dispatch_source_t mySource = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fileDescriptor, dispatchFlags, myQueue);
dispatch_source_set_event_handler(mySource, ^{
[self checkForFile];
});
dispatch_resume(mySource);
尽可能对系统提供的服务使用事件通知。表5-1列出了常见系统通知及其相应的编码方法。
表5-1获取系统服务的事件通知
要通知的事件 | 获取事件通知的方法 | 描述于 |
---|---|---|
文件更新 | 配置调度源 | Dispatch Sources in Concurrency Programming Guide |
对系统范围文件或目录的更新 | 使用文件系统事件API创建事件流。 | File System Programming Guide |
网络事件 | 使用Apple推送通知服务。 | Local and Remote Notification Programming Guide |
Use Bonjour. | DNS Service Discovery Programming Guide and NSNetServices and CFNetServices Programming Guide |
使用GCD工具进行同步,而不是使用计时器
Grand Central Dispatch(GCD)提供比计时器更高效的调度队列、调度信号量和其他同步功能。
清单5-2中的代码在一个线程上执行工作,而另一个线程和完成处理程序使用计时器定期检查第一个线程上的工作是否已完成。在第一个线程中的工作完成之前,第二个线程中的usleep计时器只会持续唤醒系统,以确定第一个线程中的工作是否已完成。
清单5-2不推荐:使用能量效率低下的计时器作为同步工具
BOOL workIsDone = NO;
/* thread one */
void doWork(void) {
/* wait for network ... */
workIsDone = YES;
}
/* thread two: completion handler */ /*** Not Recommended ***/
void waitForWorkToFinish(void) {
while (!workIsDone) {
usleep(100000); /* 100 ms */ /*** Not Recommended ***/
}
[WorkController workDidFinish];
}
清单5-3中的代码使用串行调度队列更有效地执行同步。
myQueue = dispatch_queue_create("com.myapp.myq", DISPATCH_QUEUE_SERIAL);
dispatch_block_t block;
block = dispatch_block_create(0, ^{
/* wait for network ... */
});
/* thread one */
void beginWork(void) {
dispatch_async(myQueue, block);
};
/* thread two */
void waitForWorkToFinish(void) {
dispatch_block_wait(block, DISPATCH_TIME_FOREVER);
Block_release(block);
[WorkController workDidFinish];
};
在不连续唤醒系统的情况下,线程2上的completion方法会等待第一个线程上的工作完成
类似地,清单5-4中的代码演示了如何在一个线程上执行长时间运行的操作,以及在长时间运行的操作完成后在另一个线程上执行额外的工作。例如,此技术可用于防止在应用程序的主线程上发生阻塞工作。
清单5-4推荐:使用异步调度队列在多个线程上执行工作
dispatch_async(thread2_queue) {
/* do long work */
dispatch_async(thread1_queue) {
/* continue with next work */
}
};
有关使用调度队列和Grand Central dispatch的其他同步功能的更多信息,请看see Concurrency Programming Guide和Grand Central Dispatch (GCD) 参考,也要看 Synchronization 在 Threading Programming Guide.为了获得最佳的能源效率,需要对调度队列进行分类,以便按系统进行优先级排序。看Specify a QoS for Dispatch Queues and Blocks 在 Prioritize Work with Quality of Service Classes.
如果你必须使用计时器,就要有效地使用它
游戏和其他图形密集型应用程序通常依赖计时器启动屏幕或动画更新。许多编程接口在指定的时间段内延迟进程。传递相对或绝对截止日期的任何方法或函数都可能是计时器API。例如:
高级计时器api包括分派计时器源、CFRunLoopTimerCreate和其他CFRunLoopTimer函数、NSTimer类和performSelector:withObject:afterDelay:method。
低级计时器api包括sleep、usleep、nanosleep、pthread_cond_timedwait、select、poll、kevent、dispatch_after和dispatch_semaphore_wait等函数。
如果您确定您的应用程序需要计时器,请遵循以下准则以获取最少的能量:
通过指定合适的超时,经济地使用计时器。
当不再需要重复计时器时使其失效。
设置计时器应在何时触发的公差。
指定适当的超时
许多函数都包含一个timeout参数,并在达到超时时退出。将一个时间间隔传递给这些函数会使它们作为计时器运行,并消耗计时器的所有能量。
如果你的应用程序使用了不适当的超时值,那么这种使用可能会浪费能量。例如,在清单5-5中的代码中,将当前时间(DISPATCH_Time_NOW)的500纳秒超时值传递给DISPATCH_semaphore_wait函数。在发出信号之前,代码不会执行任何有用的工作,而dispatch_semaphore_wait会持续超时。.如果你的应用程序使用DISPATCH_TIME_FOREVER常量,则该用法可能会无限期地阻止某个函数,从而只允许在需要时恢复该函数。清单5-6中的代码将DISPATCH_TIME_ever常量传递给DISPATCH_semaphore_wait。函数将阻塞,直到收到信号量为止。
清单5-5不推荐:设置应用程序不执行的超时
while (YES) {
dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, 500 * NSEC_PER_SEC);
long semaphoreReturnValue = dispatch_semaphore_wait(mySemaphore, timeout);
if (havePendingWork) {
[self doPendingWork];
}
}
清单5-6建议:无限期阻塞,直到收到信号量
while (YES) {
dispatch_time_t timeout = DISPATCH_TIME_FOREVER;
long semaphoreReturnValue = dispatch_semaphore_wait(mySemaphore, timeout);
if (havePendingWork) {
[self doPendingWork];
}
}
在大多数情况下,无限期阻塞(如清单5-6所示)比指定时间值更合适。但是,如果您的应用程序确实需要等待超时,请指定表示有意义的状态更改的信号量值,例如错误条件或网络超时。
使不再需要的重复计时器失效
如果使用重复计时器,请在不再需要时使其无效或取消。忘记停止计时器浪费了大量的精力,是最简单的问题之一。
清单5-7中的代码使用了一个重复的NSTimer计时器。当不再需要计时器时,代码调用invalidate方法来停止计时器再次启动,从而避免不必要的能量消耗。
清单5-7建议:在不再需要计时器时使其失效
NSTimer *myTimer = [[NSTimer alloc] initWithFireDate:date
interval:1.0
target:self
selector:@selector(timerFired:)
userInfo:nil
repeats:YES];
/* Do work until the timer is no longer needed */
[myTimer invalidate]; /* Recommended */
对于重复调度计时器,当不再需要时,使用dispatch_source_cancel功能取消计时器。对于重复的CFRunLoop计时器,请使用CFRunLoopTimerInvalidate函数。
指定系统范围内批处理计时器的公差
指定计时器触发时的精度公差。该系统将利用这种灵活性,在其公差范围内将计时器的执行移动少量时间,以便可以同时执行多个计时器。使用这种方法可以显著增加处理器空闲的时间,而用户检测不到系统响应性的变化。
可以使用setTolerance:方法指定计时器的公差,如清单5-8所示。10%的公差由setTolerance:0.3与interval:3.0设置。
清单5-8建议:为NSTimer计时器设置一个公差
[myTimer setTolerace:0.3];
[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode];
清单5-9中的示例显示了如何使用dispatch_source_set_timer函数的最后一个参数设置10%的公差
清单5-9建议:设置分派计时器的公差
dispatch_source_t myDispatchSourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, myQueue);
dispatch_source_set_timer(myDispatchSourceTimer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, NSEC_PER_SEC / 10);
dispatch_source_set_event_handler(myDispatchSourceTimer, ^{
[self timerFired];
}
);
dispatch_resume(myDispatchSourceTimer);
您可以使用CFRunLoopTimerSetTolerance函数为CFRunLoop计时器指定计时器间隔的10%的公差,如清单5-10所示。与CFRunLoopTimerCreate的第三个参数相比,cfrunlooptimersettlerance的第二个参数设置了10%的公差。
清单5-10建议:设置CFRunLoop计时器的公差
CFRunLoopTimerRef myRunLoopTimer = CFRunLoopTimerCreate(kCFAllocatorDefault, fireDate, 2.0, 0, &timerFired, NULL);
CFRunLoopTimerSetTolerance(myRunLoopTimer, 0.2);
CFRunLoopAddTimer(CFRunLoopGetCurrent(), myRunLoopTimer, kCFRunLoopDefaultMode);
为计时器指定公差后,它可以在其计划开始日期和计划开始日期加上公差之间的任何时间开始。计时器不会在预定的开始日期前开始。对于重复计时器,下一个开始日期总是从原始开始日期开始计算,以便使将来的开始时间与原始计划保持一致。
一般准则是将公差设置为重复计时器间隔的至少10%,如以上示例所示。即使是少量的容忍度也会对应用程序的能量使用产生显著的积极影响。
你不能指望计时器在你要求的精确纳秒内开始,即使没有公差。这个系统尽力满足你的需要,但不能保证准确的开始时间。系统甚至可以对没有指定数量的计时器应用少量的公差,但该公差通常不足以允许在一次唤醒期间触发多个计时器。