六、 电量消耗
移动设备上电量一直是比较敏感的问题,如果用户在某款 App 的时候发现耗电量严重、手机发热严重,那么用户很大可能会马上卸载这款 App。所以需要在开发阶段关心耗电量问题。
一般来说遇到耗电量较大,我们立马会想到是不是使用了定位、是不是使用了频繁网络请求、是不是不断循环做某件事情?
开发阶段基本没啥问题,我们可以结合 Instrucments 里的 Energy Log 工具来定位问题。但是线上问题就需要代码去监控耗电量,可以作为 APM 的能力之一。
1. 如何获取电量
在 iOS 中,IOKit 是一个私有框架,用来获取硬件和设备的详细信息,也是硬件和内核服务通信的底层框架。所以我们可以通过 IOKit 来获取硬件信息,从而获取到电量信息。步骤如下:
- 首先在苹果开放源代码 opensource 中找到 IOPowerSources.h、IOPSKeys.h。在 Xcode 的
Package Contents里面找到IOKit.framework。 路径为/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/IOKit.framework - 然后将 IOPowerSources.h、IOPSKeys.h、IOKit.framework 导入项目工程
- 设置 UIDevice 的 batteryMonitoringEnabled 为 true
- 获取到的耗电量精确度为 1%
2. 定位问题
通常我们通过 Instrucments 里的 Energy Log 解决了很多问题后,App 上线了,线上的耗电量解决就需要使用 APM 来解决了。耗电地方可能是二方库、三方库,也可能是某个同事的代码。
思路是:在检测到耗电后,先找到有问题的线程,然后堆栈 dump,还原案发现场。
在上面部分我们知道了线程信息的结构, thread_basic_info 中有个记录 CPU 使用率百分比的字段 cpu_usage。所以我们可以通过遍历当前线程,判断哪个线程的 CPU 使用率较高,从而找出有问题的线程。然后再 dump 堆栈,从而定位到发生耗电量的代码。详细请看 3.2 部分。
- (double)fetchBatteryCostUsage
{
// returns a blob of power source information in an opaque CFTypeRef
CFTypeRef blob = IOPSCopyPowerSourcesInfo();
// returns a CFArray of power source handles, each of type CFTypeRef
CFArrayRef sources = IOPSCopyPowerSourcesList(blob);
CFDictionaryRef pSource = NULL;
const void *psValue;
// returns the number of values currently in an array
int numOfSources = CFArrayGetCount(sources);
// error in CFArrayGetCount
if (numOfSources == 0) {
NSLog(@"Error in CFArrayGetCount");
return -1.0f;
}
// calculating the remaining energy
for (int i=0; i<numOfSources; i++) {
// returns a CFDictionary with readable information about the specific power source
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.0f;
}
3. 开发阶段针对电量消耗我们能做什么
CPU 密集运算是耗电量主要原因。所以我们对 CPU 的使用需要精打细算。尽量避免让 CPU 做无用功。对于大量数据的复杂运算,可以借助服务器的能力、GPU 的能力。如果方案设计必须是在 CPU 上完成数据的运算,则可以利用 GCD 技术,使用 dispatch_block_create_with_qos_class(<#dispatch_block_flags_t flags#>, dispatch_qos_class_t qos_class, <#int relative_priority#>, <#^(void)block#>)() 并指定 队列的 qos 为 QOS_CLASS_UTILITY。将任务提交到这个队列的 block 中,在 QOS_CLASS_UTILITY 模式下,系统针对大量数据的计算,做了电量优化
除了 CPU 大量运算,I/O 操作也是耗电主要原因。业界常见方案都是将「碎片化的数据写入磁盘存储」这个操作延后,先在内存中聚合吗,然后再进行磁盘存储。碎片化数据先聚合,在内存中进行存储的机制,iOS 提供 NSCache 这个对象。
NSCache 是线程安全的,NSCache 会在达到达预设的缓存空间的条件时清理缓存,此时会触发 - (**void**)cache:(NSCache *)cache willEvictObject:(**id**)obj; 方法回调,在该方法内部对数据进行 I/O 操作,达到将聚合的数据 I/O 延后的目的。I/O 次数少了,对电量的消耗也就减少了。
NSCache 的使用可以查看 SDWebImage 这个图片加载框架。在图片读取缓存处理时,没直接读取硬盘文件(I/O),而是使用系统的 NSCache。
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
return [self.memoryCache objectForKey:key];
}
- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key {
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
return diskImage;
}
可以看到主要逻辑是先从磁盘中读取图片,如果配置允许开启内存缓存,则将图片保存到 NSCache 中,使用的时候也是从 NSCache 中读取图片。NSCache 的 totalCostLimit、countLimit 属性,
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g; 方法用来设置缓存条件。所以我们写磁盘、内存的文件操作时可以借鉴该策略,以优化耗电量。
文章内容过长,拆分为多个篇章,请自行点击查看,如果想整体连贯查看,请访问这里