Minimize Timer Use
You can reduce your app’s energy usage by implementing energy-efficient APIs instead of timers. The NSURLSession
API, for example, provides the ability to perform out-of-process background URL sessions and receive notifications when they are complete. See Defer Networking. If you must use timers, employ them efficiently.
The High Cost of Timers
A timer lets you schedule a delayed or periodic action. A timer waits until a certain interval has elapsed and then fires, performing a specific action such as sending a message to its target object. Waking the system from an idle state incurs an energy cost when the CPU and other systems are awakened from their low-power, idle states. If a timer causes the system to wake, it incurs that cost.
Apps often use timers unnecessarily. If you use timers in your app, consider whether you truly need them. For example, some apps use timers to poll for state changes when they should respond to events instead. Other apps use timers as synchronization tools when they should use semaphores or other locks to achieve the greatest efficiency. Some timers are executed without suitable timeouts, causing them to continue firing when they’re no longer needed. Regardless of the scenario, if there are many timer-invoked wakeups, the energy impact is high.
Get Event Notifications Without Using Timers
Some apps use timers to monitor for changes to file contents, network availability, and other state changes. Timers prevent the CPU from going to or staying in the idle state, which increases energy usage and consumes battery power.
Instead of using timers to watch for events, use a more efficient service, such as a dispatch source. See Listing 5-1.
Objective-C
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);
Swift
let myFile = @"/Path/To/File"
let fileDescriptor = open(myFile.fileSystemRepresentation, O_EVTONLY)
let myQueue = dispatch_get_main_queue()
let dispatchFlags = DISPATCH_VNODE_DELETE | DISPATCH_VNODE_WRITE
let mySource = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, fileDescriptor, dispatchFlags, myQueue)
dispatch_source_set_event_handler(mySource) {
self.checkForFile()
}
dispatch_resume(mySource)
Use event notifications for system-provided services whenever possible. Table 5-1 provides a list of common system notifications along with their corresponding coding approaches.
Event to be notified about |
Approach to follow for obtaining event notifications |
Described in |
---|---|---|
Updates to files |
Configure a dispatch source. |
|
Updates to systemwide files or directories |
Create an event stream with the File System Events API. |
|
Network events |
Use the Apple Push Notification service. |
|
|
Use Bonjour. |
DNS Service Discovery Programming Guide and NSNetServices and CFNetServices Programming Guide |
Use GCD Tools for Synchronization Instead of Timers
Grand Central Dispatch (GCD) provides dispatch queues, dispatch semaphores, and other synchronization features that are more efficient than timers.
The code in Listing 5-2 performs work on one thread while another thread and a completion handler use a timer to periodically check whether work on the first thread has completed. Until the work in the first thread is completed, the usleep
timer in the second thread continually wakes the system only to determine whether the work in the first thread has completed.
Objective-C
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];
}
Swift
var workIsDone = false
/* thread one */
func doWork() {
/* wait for network ... */
workIsDone = true
}
/* thread two: completion handler */ /*** Not Recommended ***/
func waitForWorkToFinish() {
while (!workIsDone) {
usleep(100000) /* 100 ms */ /*** Not Recommended ***/
}
WorkController.workDidFinish()
}
The code in Listing 5-3 performs synchronization much more efficiently with a serial dispatch queue.
Objective-C
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];
};
Swift
let myQueue = dispatch_queue_create("com.myapp.myq", DISPATCH_QUEUE_SERIAL)
let block = dispatch_block_create(0) {
/* wait for network ... */
}
/* thread one */
func beginWork() {
dispatch_async(myQueue, block)
}
/* thread two */
func waitForWorkToFinish() {
dispatch_block_wait(block, DISPATCH_TIME_FOREVER)
WorkController.workDidFinish()
}
Without continually waking the system, the completion method on thread two waits for the work on the first thread to finish.
Similarly, the code in Listing 5-4 demonstrates how to perform a long-running operation on one thread, and additional work on another thread once the long-running operation completes. This technique could be used, for example, to prevent blocking work from occurring on the main thread of your app.
Objective-C
dispatch_async(thread2_queue) {
/* do long work */
dispatch_async(thread1_queue) {
/* continue with next work */
}
};
Swift
dispatch_async(thread2_queue) {
/* do long work */
dispatch_async(thread1_queue) {
/* continue with next work */
}
}
If You Must Use a Timer, Employ It Efficiently
Games and other graphics-intensive apps often rely on timers to initiate screen or animation updates. Many programming interfaces delay processes for specified periods of time. Any method or function to which you pass a relative or absolute deadline is probably a timer API. For example:
High-level timer APIs include dispatch timer sources,
CFRunLoopTimerCreate
and other CFRunLoopTimer functions, theNSTimer
class, and theperformSelector:withObject:afterDelay:
method.Low-level timer APIs include the functions
sleep
,usleep
,nanosleep
,pthread_cond_timedwait
,select
,poll
,kevent
,dispatch_after
, anddispatch_semaphore_wait
.
If you determine that your app requires a timer, follow these guidelines for drawing the least amount of energy:
Use timers economically by specifying suitable timeouts.
Invalidate repeating timers when they’re no longer needed.
Set tolerances for when timers should fire.
Specify Suitable Timeouts
Many functions include a timeout parameter and exit when the timeout is reached. Passing a time interval to these functions causes them to operate as timers, with all the energy consumption of timers.
If your app uses an inappropriate timeout value, that usage can waste energy. For example, in the code in Listing 5-5, a timeout value of 500 nanoseconds from the current time (DISPATCH_TIME_NOW
) is passed to the dispatch_semaphore_wait
function. Until the semaphore is signaled, the code performs no useful work while dispatch_semaphore_wait
continually times out.
If your app uses the DISPATCH_TIME_FOREVER
constant, that usage can block a function indefinitely, allowing the function to resume only when needed. The code in Listing 5-6 passes the DISPATCH_TIME_FOREVER
constant to dispatch_semaphore_wait
. The function blocks until it receives the semaphore.
Objective-C
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];
}
}
Swift
repeat {
let timeout = dispatch_time(DISPATCH_TIME_NOW, 500 * Double(NSEC_PER_SEC))
let semaphoreReturnValue = dispatch_semaphore_wait(mySemaphore, timeout)
if (havePendingWork) {
self.doPendingWork()
}
} while true
Objective-C
while (YES) {
dispatch_time_t timeout = DISPATCH_TIME_FOREVER;
long semaphoreReturnValue = dispatch_semaphore_wait(mySemaphore, timeout);
if (havePendingWork) {
[self doPendingWork];
}
}
Swift
repeat {
let timeout = DISPATCH_TIME_FOREVER
let semaphoreReturnValue = dispatch_semaphore_wait(mySemaphore, timeout)
if (havePendingWork) {
self.doPendingWork()
}
} while true
In most cases, blocking indefinitely (as in Listing 5-6) is more suitable than specifying a time value. But if your app does need to wait for a timeout, specify a semaphore value that represents a meaningful state change, such as an error condition or a network timeout.
Invalidate Repeating Timers You No Longer Need
If you use a repeating timer, invalidate or cancel it when you no longer need it. Forgetting to stop timers wastes lots of energy, and is one of the simplest problems to fix.
The code in Listing 5-7 uses a repeating NSTimer
timer. When the timer is no longer needed, the code calls the invalidate
method to stop the timer from firing again, avoiding unnecessary energy use.
Objective-C
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 */
Swift
var myTimer = NSTimer.initWithFireDate(date, interval: 1.0, target: self, selector:"timerFired:", userInfo:nil repeats: true)
/* Do work until the timer is no longer needed */
myTimer.invalidate() /* Recommended */
For a repeating dispatch timer, use the dispatch_source_cancel
function to cancel the timer when it’s no longer needed. For a repeating CFRunLoop
timer, use the CFRunLoopTimerInvalidate
function.
Specify a Tolerance for Batching Timers Systemwide
Specify a tolerance for the accuracy of when your timers fire. The system will use this flexibility to shift the execution of timers by small amounts of time—within their tolerances—so that multiple timers can be executed at the same time. Using this approach dramatically increases the amount of time that the processor spends idling while users detect no change in system responsiveness.
You can use the setTolerance:
method to specify a tolerance for your timer, as shown in Listing 5-8. The tolerance of ten percent is set by setTolerance:0.3
compared to interval:3.0
.
Objective-C
[myTimer setTolerace:0.3];
[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode];
Swift
myTimer.tolerance(0.3)
NSRunLoop.currentRunLoop().addTimer(myTimer, forMode:NSDefaultRunLoopMode)
The example in Listing 5-9 shows how you can set a tolerance of ten percent using the last parameter of the dispatch_source_set_timer
function.
Objective-C
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);
Swift
let myDispatchSourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, myQueue)
dispatch_source_set_timer(myDispatchSourceTimer, DISPATCH_TIME_NOW, 1 * Double(NSEC_PER_SEC), Double(NSEC_PER_SEC) / 10)
dispatch_source_set_event_handler(myDispatchSourceTimer) {
self.timerFired()
}
dispatch_resume(myDispatchSourceTimer)
You can specify a tolerance of ten percent of the timer interval for CFRunLoop
timers using the CFRunLoopTimerSetTolerance
function, as shown in Listing 5-10. The tolerance of ten percent is set by the second argument to CFRunLoopTimerSetTolerance
, compared with the third argument to CFRunLoopTimerCreate
.
Objective-C
CFRunLoopTimerRef myRunLoopTimer = CFRunLoopTimerCreate(kCFAllocatorDefault, fireDate, 2.0, 0, &timerFired, NULL);
CFRunLoopTimerSetTolerance(myRunLoopTimer, 0.2);
CFRunLoopAddTimer(CFRunLoopGetCurrent(), myRunLoopTimer, kCFRunLoopDefaultMode);
Swift
myRunLoopTimer = CFRunLoopTimerCreate(kCFAllocatorDefault, fireDate, 2.0, 0, 0, &timerFired, NULL)
CFRunLoopTimerSetTolerance(myRunLoopTimer, 0.2)
CFRunLoopAddTimer(CFRunLoopGetCurrent(), myRunLoopTimer, kCFRunLoopDefaultMode)
After you specify a tolerance for a timer, it may fire anytime between its scheduled fire date and the scheduled fire date, plus the tolerance. The timer won’t fire before the scheduled fire date. For repeating timers, the next fire date is always calculated from the original fire date in order to keep future fire times on track with their original schedule.
A general guideline is to set the tolerance to at least ten percent of the interval for a repeating timer, as in the examples above. Even a small amount of tolerance has a significant positive impact on the energy usage of your app.
Prioritize Work with Quality of Service Classes
Copyright © 2018 Apple Inc. All rights reserved. Terms of Use | Privacy Policy | Updated: 2016-09-13