对于生活离不开手机的我们来说,手机的电量就是一条重要的生命线,一般来说,当电量低于 20% 的时候,我们的心总是那么揪着。作为一个开发者来说,我们应该为用户的手机省电,让用户有限的电量能够更长时间的使用我们开发的 APP,对用户,对我们开发者来说是两全其美的方案。所以 APP 的电量消耗也应该是性能优化的点。
案例
还是以 raywenderlich 的 Catstagram APP 作为分析案例。该案例是一个带有图片的列表。

值的注意的是在我的开发环境下 Energy 需要运行在真机设备上,我的开发环境是 Xcode 8.3.2 , iPhone 6 (10.3.1)。
使用 Energy Impact
Energy Impact 是 Xcode 自带的一个用于查看设备电量开销概况的工具。

如上图所示,点击 Xcode 左边的 Energy Impact 栏目就可以看到设备上正在运行的 APP 的电量消耗水平。

看图左边有 CPU ,Network , Location , GPU, Background 五个指标,这 5 个 指标也是能耗大户,右边的表格中的若是被灰色填充,那么就意味着在那个时刻,该指标是活跃的。比如图上所示 CPU 和 Network 一直都是被灰色填充,那么就意味着 CPU 和 Network 一直处于活跃状态。顶部有蓝色和红色的柱形图,红色是Overhead指标,表示除这个 APP 外,系统的其他电量消耗,蓝色是Cost,表示这个 APP 的电量消耗。关于更多的 Energy Impact 信息可以参考 Apple 的官方文档 https://developer.apple.com/library/content/documentation/Performance/Conceptual/EnergyGuide-iOS/MonitorEnergyWithXcode.html 。这里就不再累赘。

APP 运行一段时间,滑动几次列表之后, APP 的能耗就变的非常高,从图中就可以看出,APP 在静止状态的电量高消耗情况肯定是不正常的,Energy Impact 只能看出是否有问题,而不能指出哪里可能有问题。那么这个时候就要祭出 Instruments 利器了。
Instruments 之 Energy
Command + I 运行 Instruments 选择 Energy Log 模板。


看左边的 Energy Log 的指标有 Energy,CPU,Network等等应有尽有。
点击开始按钮,录制 APP 运行情况

从图中可以看出整个 APP 的能量消耗情况,但是存在一个问题,这个问题就是我们已经知道了APP 的这些能量消耗情况,但是怎么知道要去修改哪里的代码呢?这个时候我们需要 Time Profiler 工具。

如上图所示,我们添加了 Time Profiler 工具用来记录 APP 在某个时间段的代码运行情况。
万事具备之后,我们重新开始录制 APP 的运行情况。


Energy Log 结合 Timer Profiler 的使用,避免干扰我们隐藏系统库内容,显示我们的代码调用。

按照代码执行时间的权重比,找到了 CatPhotoTableViewCell 的 panImage(with yRotation: CGFloat) 方法。通过代码追溯,我们找到了 CatFeedViewController.swift 文件的 viewDidLoad() 方法,找到了 panImage(with yRotation: CGFloat) 方法被频繁调用的地方
motionManager.startDeviceMotionUpdates(to: .main, withHandler:{ deviceMotion, error in
guard let deviceMotion = deviceMotion else { return }
self.lastY = deviceMotion.rotationRate.y
let xRotationRate = CGFloat(deviceMotion.rotationRate.x)
let yRotationRate = CGFloat(deviceMotion.rotationRate.y)
let zRotationRate = CGFloat(deviceMotion.rotationRate.z)
print("y \(yRotationRate) and x \(xRotationRate) and z\(zRotationRate)")
if abs(yRotationRate) > (abs(xRotationRate) + abs(zRotationRate)) {
for cell in self.tableView.visibleCells as! [CatPhotoTableViewCell] {
cell.panImage(with: yRotationRate)
}
}
})
这段代码的关键在于 self.lastY = deviceMotion.rotationRate.y 这个语句,无论 deviceMotion.rotationRate.y 变化多大,都执行后面的代码,正常应该是 deviceMotion.rotationRate.y 的变化范围超过多少的时候才执行后面的代码,所以优化如下
motionManager.startDeviceMotionUpdates(to: .main, withHandler:{ deviceMotion, error in
guard let deviceMotion = deviceMotion else { return }
guard abs(self.lastY - deviceMotion.rotationRate.y) > 0.1 else { return }
let xRotationRate = CGFloat(deviceMotion.rotationRate.x)
let yRotationRate = CGFloat(deviceMotion.rotationRate.y)
let zRotationRate = CGFloat(deviceMotion.rotationRate.z)
print("y \(yRotationRate) and x \(xRotationRate) and z\(zRotationRate)")
if abs(yRotationRate) > (abs(xRotationRate) + abs(zRotationRate)) {
for cell in self.tableView.visibleCells as! [CatPhotoTableViewCell] {
cell.panImage(with: yRotationRate)
}
}
})
修改 self.lastY = deviceMotion.rotationRate.y 的逻辑为 guard abs(self.lastY - deviceMotion.rotationRate.y) > 0.1 else { return }
。当 deviceMotion.rotationRate.y 变化范围超过 0.1 的时候才执行后面的代码逻辑。修改完代码之后进行验证修改效果。

使用 Energy Impact 进行验证之后,发现能耗还是非常高,降不下来。那么接下来就继续使用 Instruments 查找原因。

发现 CatFeedViewController 的 sendLogs() 也是占用了大量的 CPU 时间,接下来使用 Xcode 查看代码。通过代码追溯,找到了 CatFeedViewController 的init() 方法。
init() {
super.init(nibName: nil, bundle: nil)
navigationItem.title = "Catstagram"
tableView.autoresizingMask = UIViewAutoresizing.flexibleWidth;
tableView.delegate = self
tableView.dataSource = self
let timer = Timer(timeInterval: 1.0, target: self, selector: #selector(CatFeedViewController.sendLogs), userInfo: nil, repeats: true)
RunLoop.main.add(timer, forMode: .commonModes)
}
在这个init() 方法里面发现了一个惊人的代码,有一个定时器每隔 1 s 发起一次 sendlog 的网络请求。不用怀疑了,肯定就是这个坑爹的代码消耗了大量的电量。正常的发送 log 操作应该是在 APP 启动完成的时候发送上次的 log 或者在 APP 进入 applicationWillResignActive 的回调方法发送 log。我们的修改方案是在 APP 进入 applicationWillResignActive 的回调方法发送 log。打开 AppDelegate.swift 文件,添加如下代码同时删除 CatFeedViewController 的init() 方法里面的 sendlog 定时器。
func applicationWillResignActive(_ application: UIApplication) {
rootVC.sendLogs()
}
接下来就是验证修改效果了,使用 Energy Impact 这个工具来验证,对于 验证 APP 的能耗概况来说, Energy Impact 工具足以满足需求。

现在 APP 的能耗是处于低水平,并且 Network 斌不是一直处于活跃状态。

将 APP 退到后台,再进入前台,触发 APP 的 sendlog 操作。这个时候 APP 的能耗进入高等级,但是随后下降到低等级能耗。这个是 APP 的正常表现。
总结
APP 性能优化中,能耗优化决定了用户在同样的电量消耗情况下能使用你的 APP 多长时间。能耗优化的一般步骤如下
1、使用 Energy Impact 查看 APP 能耗概况
2、若是存在高能耗情况,使用 Instruments 的 Energy Log 模板进行细致验证,并配合 Time Profiler 模板抓取代码的运行细节。
3、根据代码的运行细节,判断潜在的问题点,然后修改代码
4、验证修改效果,若是无效,那么重复 2 - 4 步骤
参考
本文是 raywenderlich 的课程笔记,内容参考 Practical Instruments 课程
1、demo下载地址 https://files.betamax.raywenderlich.com/attachments/videos/789/9560e62e-96d3-47e5-b604-5d20c72bf9ee.zip
2、 Energy Log 课程地址
https://videos.raywenderlich.com/courses/74-practical-instruments/lessons/7
3、Energy Impact 官方文档
https://developer.apple.com/library/content/documentation/Performance/Conceptual/EnergyGuide-iOS/MonitorEnergyWithXcode.html
网友评论
还是用Time Profiler 找到消耗cpu时间最大的执行函数。。。