如何降低App耗电量?

838 阅读4分钟

这是我参与8月更文挑战的第24天,活动详情查看: 8月更文挑战

我们的手机设备,电量都十分有限,在App开发过程中,如果不注意电量的消耗,如果用户发现我们的App是一个耗电大户的时候,那么我们的App将会被用户无情卸载;所以,我们需要检查自己的App是否有大量耗电的问题存在?

耗电的原因有很多,如果每次遇到耗电过多的问题,我们都从头查找一番的话,那效率必然十分低下。

首先,我们需要获取到电量,然后才能发现耗电量问题出在什么地方?

如何获取电量

iOS开发过程中,IOKit.framework是专门用来跟硬件或者内核进行服务通信的。所以,我们可以通过这个库来获取硬件信息,进而获取到电量。但是在使用IOKit的时候,需要注意进行以下步骤:

  • 1、把IOPowerSources.hIOPSKeys.hIOKit三个文件导入到工程中;
  • 2、把batteryMonitoringEnabled设置为true;
  • 3、通过代码获取电量信息,精确度到1%,代码实现如下:
#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.

到此为止,我们已经能够获取到电量了,接下来我们就需要去解决耗电量的问题;

怎么定位耗电问题

要想定位耗电问题,就要先确定定位方法,因为这个耗电问题有可能是其他线程引起的,也有可能是某一个第三方库引起的;所以解决这个问题的时候,我们就需要逆向的去思考这个问题;

我们不放回顾一下,在如何监控iOS的崩溃问题的文章中,我们是如何定位问题的。

也就是说,我们还是要确定在电量出现问题的过程中,哪个线程出现了问题?我们可以通过以下代码来获取到所有线程的信息:

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);

从上边代码中我们可以看出,通过task_threads函数,我们能够得到所有的线程信息数组threads,以及线程总数threadCountthreads数组中的线程信息结构体thread_basic_info里有一个记录CPU使用百分比的字段cpu_usage

thread_basic_info的结构体如下:

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_usage这个字段,我们就可以通过遍历所有线程,去查看是哪个线程的CPU使用百分比过高了。如果某个线程的CPU使用率长时间都比较高的话,比如超过了90%,就能够推断出线程是有问题的。这时,将其方法堆栈记录下来,就可以知道到底是哪段代码让我们的App耗电量过多了。

通过这个方法,我们可以快速定位问题的所在,有针对性的进行代码优化。多线程CPU使用率检查的代码完整实现如下:

// 轮询检查多个线程 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);
                }
            }
        }
    }
}

如何优化电量

上文我们已经分析了耗电的问题,但是电量的消耗也可能来自其他方面。CPU是耗电大户,引起其耗电的单一问题我们可以通过监控来解决,但是也有可能是几处小的耗电问题,最终导致了电量的大量消耗。所以,日常开发过程中,时刻关注电量的优化是很有必要的。

我们应该尽量避免让CPU进行大量的操作,复杂数据的计算应该尽量把数据上传到服务器处理,如果必须在App内处理复杂数据计算的话,可以通过GCDdispatch_block_create_with_qos_class方法指定队列的QosQOS_CLASS_UTILITY,将计算工作放到这个队列的block里。在QOS_CLASS_UTILITY这种Qos模式下,系统针对大量数据的计算,以及复杂数据处理专门做了电量的优化.

影响电量消耗的因素

除了CPU,IO操作也是耗电大户,任何的IO操作都会破坏掉低功耗状态。那么针对IO操作应该如何进行优化?

一般我们将碎片化的数据磁盘存储操作延后,现在内存中聚合,然后再进行磁盘存储。碎片化的数据进行聚合,在内存中进行存储的机制,可以使用系统自带的NSCache来完成。

NSCache是线程安全的,NSCache会在到达预设缓存空间值时清理缓存,这时会触发cache:willEvictObject:方法的回调,在这个会调中就可以进行数据的IO操作,达到将聚合的数据IO延后的目的。IO操作次数少了,对电量的消耗也就相应的减少了。

SWWebImage图片加载框架,在图片的读取缓存处理时没有直接IO,而是使用了NSCache。相关代码如下:

- (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里读取;如果没有,才会通过IO读取磁盘缓存图片。

使用了NSache内存缓存能够有效减少IO操作,我们在开发类似功能时也可以采用这样的思路,从而大大减少我们App的耗电量;

其他

Apple公司专门维护了一个电量优化的指南:Energy Efficiency Guide for iOS Apps分别从CPU,设备唤醒,网络,图形,动画等等多方面因素提出了电量优化的建议。所以,当我们使用到相关功能时,按照指南中的实践去实施,基本上可以保证不会引起耗电量大的问题;

2017WWDCSession 238也分享了一个关于如何编写节能App的主题Writing Energy Efficient Apps,感兴趣的小伙伴可以看一下;