这是我参与8月更文挑战的第24天,活动详情查看: 8月更文挑战
我们的手机设备,电量都十分有限,在App开发过程中,如果不注意电量的消耗,如果用户发现我们的App是一个耗电大户的时候,那么我们的App将会被用户无情卸载;所以,我们需要检查自己的App是否有大量耗电的问题存在?
耗电的原因有很多,如果每次遇到耗电过多的问题,我们都从头查找一番的话,那效率必然十分低下。
首先,我们需要获取到电量,然后才能发现耗电量问题出在什么地方?
如何获取电量
在iOS开发过程中,IOKit.framework是专门用来跟硬件或者内核进行服务通信的。所以,我们可以通过这个库来获取硬件信息,进而获取到电量。但是在使用IOKit的时候,需要注意进行以下步骤:
- 1、把
IOPowerSources.h、IOPSKeys.h和IOKit三个文件导入到工程中; - 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,以及线程总数threadCount。threads数组中的线程信息结构体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内处理复杂数据计算的话,可以通过GCD的dispatch_block_create_with_qos_class方法指定队列的Qos为QOS_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,设备唤醒,网络,图形,动画等等多方面因素提出了电量优化的建议。所以,当我们使用到相关功能时,按照指南中的实践去实施,基本上可以保证不会引起耗电量大的问题;
在2017年WWDC中Session 238也分享了一个关于如何编写节能App的主题Writing Energy Efficient Apps,感兴趣的小伙伴可以看一下;