App电量消耗优化

App中的耗电大户:CPU和I/O操作

如何获取电量

IOKit framework是专门用于跟硬件或内核服务通信的。所以,我们可以通过IOKit framework来获取硬件信息,进而获取到电量消耗信息。

使用:导入IOPowerSources.hIOPSKeys.hIOKit,将batteryMonitoringEnable设置为true,通过代码获取精确到1%的电量信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#import "IOPSKeys.h"
#import "IOPowerSources.h"

-(double) getBatteryLevel{
// 返回电量信息
CFTypeRef blob = IOPSCopyPowerSourcesInfo();
// 返回电量句柄列表数据
CFArrayRef sources = IOPSCopyPowerSourcesList(blob);
CFDictionaryRef pSource = NULL;
const void *psValue;
// 返回数组大小
int numOfSources = CFArrayGetCount(sources);
// 计算大小出错处理
if (numOfSources == 0) {
NSLog(@"Error in CFArrayGetCount");
return -1.0f;
}

// 计算所剩电量
for (int i=0; i<numOfSources; i++) {
// 返回电源可读信息的字典
pSource = IOPSGetPowerSourceDescription(blob, CFArrayGetValueAtIndex(sources, i));
if (!pSource) {
NSLog(@"Error in IOPSGetPowerSourceDescription");
return -1.0f;
}
psValue = (CFStringRef) CFDictionaryGetValue(pSource, CFSTR(kIOPSNameKey));

int curCapacity = 0;
int maxCapacity = 0;
double percentage;

psValue = CFDictionaryGetValue(pSource, CFSTR(kIOPSCurrentCapacityKey));
CFNumberGetValue((CFNumberRef)psValue, kCFNumberSInt32Type, &curCapacity);

psValue = CFDictionaryGetValue(pSource, CFSTR(kIOPSMaxCapacityKey));
CFNumberGetValue((CFNumberRef)psValue, kCFNumberSInt32Type, &maxCapacity);

percentage = ((double) curCapacity / (double) maxCapacity * 100.0f);
NSLog(@"curCapacity : %d / maxCapacity: %d , percentage: %.1f ", curCapacity, maxCapacity, percentage);
return percentage;
}
return -1;
}

诊断CPU造成的电量消耗

  1. 获取所有线程信息

    1
    2
    3
    4
    thread_act_array_t threads;
    mach_msg_type_number_t threadCount = 0;
    const task_t thisTask = mach_task_self();
    kern_return_t kr = task_threads(thisTask, &threads, &threadCount);
  2. 检查多线程的CPU使用情况,找到CPU占用高的线程,获取线程堆栈信息,并优化线程操作。

    线程基本信息结构体:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct thread_basic_info {
    time_value_t user_time; /* user 运行的时间 */
    time_value_t system_time; /* system 运行的时间 */
    integer_t cpu_usage; /* CPU 使用百分比 */
    policy_t policy; /* 有效的计划策略 */
    integer_t run_state; /* run state (see below) */
    integer_t flags; /* various flags (see below) */
    integer_t suspend_count; /* suspend count for thread */
    integer_t sleep_time; /* 休眠时间 */
    };

    通过遍历所有线程,去查看是哪个线程的 CPU 使用百分比过高了。如果某个线程的CPU使用率长时间都比较高的话,比如超过了90%,就能够推断出它是有问题的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    // 轮询检查多个线程 CPU 情况
    + (void)updateCPU {
    thread_act_array_t threads;
    mach_msg_type_number_t threadCount = 0;
    const task_t thisTask = mach_task_self();
    kern_return_t kr = task_threads(thisTask, &threads, &threadCount);
    if (kr != KERN_SUCCESS) {
    return;
    }
    for (int i = 0; i < threadCount; i++) {
    thread_info_data_t threadInfo;
    thread_basic_info_t threadBaseInfo;
    mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;
    if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
    threadBaseInfo = (thread_basic_info_t)threadInfo;
    if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {
    integer_t cpuUsage = threadBaseInfo->cpu_usage / 10;
    if (cpuUsage > 90) {
    //cup 消耗大于 90 时打印和记录堆栈
    NSString *reStr = smStackOfThread(threads[i]);
    //记录数据库中
    [[[SMLagDB shareInstance] increaseWithStackString:reStr] subscribeNext:^(id x) {}];
    NSLog(@"CPU useage overload thread stack:\n%@",reStr);
    }
    }
    }
    }
    }

    IO操作优化

    任何的 I/O 操作,都会破坏掉低功耗状态。针对优化 I/O 操作,普遍的做法是,将碎片化的数据磁盘存储操作延后,先在内存中聚合,然后再进行磁盘存储。碎片化的数据进行聚合,在内存中进行存储的机制,可以使用系统自带的NSCache来完成。

    NSCache是线程安全的,NSCache会在到达预设缓存空间值时清理缓存,这时会触发cache:willEvictObject:方法的回调,在这个回调里就可以对数据进行 I/O 操作,达到将聚合的数据 I/O 延后的目的。I/O 操作的次数减少了,对电量的消耗也就减少了。比如SDWebImage,在图片的读取缓存处理时没有直接使用I/O,而是使用了NSCache

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
    return [self.memCache objectForKey:key];
    }

    - (UIImage *)imageFromDiskCacheForKey:(NSString *)key {
    // 检查 NSCache 里是否有
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
    return image;
    }
    // 从磁盘里读
    UIImage *diskImage = [self diskImageForKey:key];
    if (diskImage && self.shouldCacheImagesInMemory) {
    NSUInteger cost = SDCacheCostForImage(diskImage);
    [self.memCache setObject:diskImage forKey:key cost:cost];
    }
    return diskImage;

    SDWebImage将获取的图片数据都放到了NSCache里,利用NSCache缓存策略进行图片缓存内存的管理。每次读取图片时,会检查NSCache是否已经存在图片数据:如果有,就直接从NSCache里读取;如果没有,才会通过 I/O 读取磁盘缓存图片。

    使用了NSCache内存缓存能够有效减少 I/O 操作,你在写类似功能时也可以采用这样的思路,让你的 App 更省电。

    其他优化

    对于大量数据的复杂计算,应该把数据传到服务器去处理,如果必须要在 App 内处理复杂数据计算,可以通过 GCD 的 dispatch_block_create_with_qos_class方法指定队列的QosQOS_CLASS_UTILITY,将计算工作放到这个队列的block里。在 QOS_CLASS_UTILITY这种 Qos模式下,系统针对大量数据的计算,以及复杂数据处理专门做了电量优化。