一、电量是怎么被消耗的?
手机电池的电量本质上就是电能。App 的各种操作最终都会驱动硬件工作,硬件工作就要消耗电能。
主要的耗电硬件:
┌────────────────────────────────────────────────────┐
│ App 的各种操作 │
├──────┬──────┬──────┬──────┬──────┬──────┬──────────┤
│ CPU │ GPU │ 网络 │ 定位 │ 屏幕 │ 传感器│ 蓝牙/NFC │
│ │ │模块 │模块 │背光 │ │ │
├──────┴──────┴──────┴──────┴──────┴──────┴──────────┤
│ 电池 │
└────────────────────────────────────────────────────┘
关键认知:硬件有两种状态------空闲态和活跃态。
空闲态几乎不耗电,活跃态耗电量可能是空闲态的 10-100 倍。电量优化的核心就是:尽量让硬件处于空闲态,减少活跃态的持续时间。
二、iOS 的电量管理机制
2.1 合并唤醒(Coalescing)
iOS 不会让硬件被频繁地"唤醒-休眠-唤醒-休眠"。它会把多个 App 的小任务合并到同一个时间窗口集中处理。
scss
不合并:
App1 ─▮─────────▮─────────▮───────── (每 10 秒唤醒一次)
App2 ──────▮─────────▮─────────▮──── (每 10 秒唤醒一次)
CPU ─▮───▮──▮──▮───▮──▮──▮───▮──▮ (被唤醒了 9 次)
合并后:
App1 ─▮─────────▮─────────▮─────────
App2 ─▮─────────▮─────────▮───────── (和 App1 对齐)
CPU ─▮─────────▮─────────▮───────── (只被唤醒 3 次)
对开发者的启示: 不要自己用精确的 Timer 去定时做事,用系统提供的 API(如 BGTaskScheduler),让系统帮你合并。
2.2 能量计量(Energy Gauges)
iOS 在系统层面持续监控每个 App 的能量消耗。如果你的 App 耗电异常:
- 设置 → 电池 里会显示高耗电
- 系统可能会限制你的后台执行时间
- App Store 审核可能因为耗电问题被拒
- 用户看到你耗电高就卸载了
2.3 后台执行限制
iOS 对后台 App 的电量管控非常严格:
| 状态 | 允许做什么 | 时间限制 |
|---|---|---|
| 前台 | 任何事 | 无限制 |
| 后台(刚切走) | 完成当前任务 | 约 30 秒(可申请延长到 ~3 分钟) |
| 后台(挂起) | 什么都不能做 | 0(被冻结) |
| 后台模式(音乐/导航/VoIP等) | 特定任务 | 持续但受监控 |
App 被挂起后,CPU 完全不分配给它,所以不耗电。 这是 iOS 比 Android 省电的核心原因之一。
三、八大耗电场景与优化
3.1 CPU ------ 最大的耗电户
为什么耗电
CPU 频率越高、负载越重、持续时间越长,耗电越多。
常见问题
| 问题 | 场景 |
|---|---|
| 死循环 / 忙等待 | while(flag) {} 没有 sleep |
| 过度计算 | 主线程做复杂的 JSON 解析、图片处理 |
| Timer 间隔太短 | 每 0.01 秒刷新一次,但界面根本看不出差别 |
| 后台还在跑 | 切后台了 Timer 还在走 |
优化策略
1. 避免忙等待,用事件驱动替代轮询
scss
❌ 轮询:每 0.1 秒检查一次数据有没有准备好
while (!dataReady) { usleep(100000); }
✅ 事件驱动:数据好了通知我
NotificationCenter / KVO / Completion Handler / Combine
2. Timer 的电量陷阱
NSTimer / DispatchSourceTimer 默认是精确触发的,会阻止 CPU 进入深度休眠。
优化方式:给 Timer 加 tolerance(容差)。
ini
timer.tolerance = interval * 0.1 // 允许 10% 的偏差
加了 tolerance 后,系统可以把你的 Timer 和其他 Timer 合并触发,减少 CPU 唤醒次数。
苹果的建议:tolerance 至少设为间隔的 10%。
3. 用合适的 QoS(Quality of Service)
iOS 的任务队列有不同的优先级,低优先级的任务系统会安排在"电量友好"的时间执行:
| QoS 级别 | 用途 | CPU 调度 |
|---|---|---|
.userInteractive |
UI 更新、动画 | 最高优先级,立即执行 |
.userInitiated |
用户触发的操作(点击后加载) | 高优先级 |
.default |
默认 | 中等 |
.utility |
长时间任务(下载、导入) | 低优先级,省电模式可能延迟 |
.background |
用户不关心何时完成(预加载、备份) | 最低,系统自行安排 |
原则:不需要立即响应的任务,用 .utility 或 .background。 系统会在电量充足或充电时才执行这些任务。
3.2 网络 ------ 隐形的耗电大户
为什么网络特别耗电
蜂窝网络模块(4G/5G)有三种功耗状态:
markdown
空闲态(Idle)─── 几乎不耗电
│ 有数据要发送
▼
升频态(Ramp Up)─── 功耗急剧上升(从空闲到全速需要 1-2 秒)
│
▼
全速态(Active)─── 高功耗传输数据
│ 数据传完
▼
拖尾态(Tail)─── 仍保持高功耗约 10-15 秒!等待可能的后续请求
│ 超时无新数据
▼
空闲态(Idle)
关键问题在"拖尾态": 传完数据后,蜂窝模块不会立刻休眠,而是保持活跃 10-15 秒等待新数据。如果你的 App 每 20 秒发一个小请求,蜂窝模块就永远无法进入空闲态。
❌ 零散请求(蜂窝模块永远醒着):
请求──拖尾──请求──拖尾──请求──拖尾──请求──拖尾
████████████████████████████████████████████ 全程高功耗
✅ 批量请求(只唤醒一次):
──────────批量请求──拖尾──────────────────────
░░░░░░░░░░█████████████░░░░░░░░░░░░░░░░░░░░ 大部分时间低功耗
优化策略
1. 请求合并(Batching)
不要每个事件都立即发网络请求。把多个请求攒在一起,一次性发送。
例如:埋点数据不要实时上报,累积 20 条或间隔 30 秒批量上报。
2. 避免轮询,用推送替代
❌ 每 30 秒轮询一次服务器检查新消息
✅ 用 APNs 推送通知客户端有新消息
3. 适配网络类型
- WiFi 比蜂窝省电得多(没有拖尾态问题)
- 大文件下载、数据同步等操作尽量在 WiFi 环境下进行
- 用
NWPathMonitor或Reachability判断当前网络类型
4. 减少数据传输量
- 开启 HTTP 压缩(gzip / br)
- 用 HTTP/2 的头部压缩
- 图片用 WebP / HEIF 替代 PNG/JPEG
- API 只返回需要的字段(GraphQL 的优势)
- 合理使用缓存(
URLCache、ETag、Last-Modified)
5. 超时和重试策略
- 设置合理的超时时间(不要太长等不来也不放手)
- 重试用指数退避(1s → 2s → 4s → 8s),不要固定间隔疯狂重试
- 失败后等 WiFi 或充电时再重试
3.3 定位 ------ 精度越高越耗电
各精度的耗电对比
| 精度 | API | 耗电 | 适用场景 |
|---|---|---|---|
| 最佳精度 | kCLLocationAccuracyBest |
极高(GPS 全速运转) | 导航 |
| 10 米 | kCLLocationAccuracyNearestTenMeters |
高 | 跑步记录 |
| 100 米 | kCLLocationAccuracyHundredMeters |
中 | 附近商家 |
| 公里级 | kCLLocationAccuracyKilometer |
低 | 天气、城市级服务 |
| 3公里级 | kCLLocationAccuracyThreeKilometers |
很低 | 粗略地理围栏 |
| 显著位置变化 | startMonitoringSignificantLocationChanges |
极低 | 只在基站切换时触发 |
GPS 芯片功耗约 25-35mW,WiFi 定位约 5-10mW,基站定位约 1-2mW。
优化策略
1. 用够了就关
开始定位 → 拿到位置 → 立即 stopUpdatingLocation
很多 App 犯的错误:开启定位后忘了关,GPS 一直在后台运转。
2. 用最低够用的精度
外卖 App 展示附近餐厅用 100 米精度足够了,不需要 Best。只有导航才需要最高精度。
3. 用"显著位置变化"替代持续定位
如果你只需要在用户换了个区域时更新内容(比如新闻 App 根据城市推荐),用 startMonitoringSignificantLocationChanges。它基于基站切换触发,几乎不额外耗电。
4. distanceFilter 过滤无意义的更新
ini
locationManager.distanceFilter = 50 // 移动 50 米以上才回调
默认是 kCLDistanceFilterNone(每次都回调),设一个合理的值可以大幅减少回调次数。
5. allowsBackgroundLocationUpdates 谨慎使用
只有导航、运动记录等真正需要后台定位的场景才开启。开启后要搭配 pausesLocationUpdatesAutomatically = true,让系统在检测到用户静止时自动暂停。
3.4 GPU / 图形渲染
耗电的渲染操作
| 操作 | 为什么耗电 |
|---|---|
| 离屏渲染 | 需要额外的帧缓冲区,GPU 要来回切换上下文 |
| 大量透明度混合 | 每一层都要计算混合,层越多越慢 |
| 大图缩小显示 | GPU 要对大图做缩放计算 |
| 实时模糊(UIBlurEffect) | 每帧都要对底层内容做高斯模糊 |
| 高帧率动画 | 120Hz 的计算量是 60Hz 的两倍 |
优化策略
1. 避免不必要的离屏渲染
markdown
触发离屏渲染的操作:
- cornerRadius + masksToBounds(圆角裁剪)
- shadow(阴影,没有设 shadowPath 时)
- mask(遮罩)
- group opacity(组透明度)
优化方式:
- 圆角:用贝塞尔曲线预先裁剪成圆角图片,或在绘图时直接画圆角
- 阴影:设置 shadowPath,避免实时计算阴影形状
- 模糊:对静态内容用截图+模糊的方式,而不是实时 UIVisualEffectView
2. 图片大小匹配显示大小
一张 3000x3000 的图显示在 100x100 的 ImageView 里,GPU 每帧都要缩放。应该在加载时就缩放到显示尺寸。
3. 降低不必要的帧率
不是所有动画都需要 60fps / 120fps。滚动和交互动画需要高帧率,但一个缓慢变化的进度条用 30fps 就够了。
objectivec
CADisplayLink 可以设置 preferredFramesPerSecond
3.5 蓝牙(BLE)
两种扫描模式的耗电差异
| 模式 | 耗电 | 说明 |
|---|---|---|
| 主动扫描(Active Scan) | 高 | 蓝牙模块持续发射扫描请求 |
| 被动监听 | 低 | 只监听广播包 |
优化策略
- 扫描到目标设备后立即停止扫描
- 设置
CBCentralManagerScanOptionAllowDuplicatesKey = NO,避免重复上报同一个设备 - 后台扫描比前台限制更严格,系统会自动降低扫描频率
- 不需要实时数据时,用
notify替代read(让外设主动通知,而不是 App 轮询读取)
3.6 后台任务
beginBackgroundTask 的正确用法
切后台时申请额外执行时间来完成当前任务:
erlang
关键要点:
① 一定要在超时回调里调用 endBackgroundTask,否则系统会杀掉你的 App
② 不要用它来"偷偷"执行长时间任务
③ 系统给的时间在 iOS 13+ 只有约 30 秒(以前是 3 分钟)
BGTaskScheduler(iOS 13+)
用于安排后台任务,系统会在合适的时间执行:
| 类型 | 用途 | 触发条件 |
|---|---|---|
BGAppRefreshTask |
数据刷新(拉新闻、同步) | 系统根据用户使用习惯决定 |
BGProcessingTask |
重计算任务(数据库清理、ML训练) | 通常在充电 + WiFi 时 |
系统会综合考虑电量、网络、充电状态、用户使用习惯来决定何时执行你的任务。 你只需要提交任务,不需要操心何时执行。
3.7 推送通知
静默推送的耗电陷阱
静默推送(content-available: 1)会唤醒 App 在后台执行代码。如果推送频率太高(比如每分钟一次),相当于 App 每分钟被唤醒一次,持续消耗 CPU 和网络。
苹果会限制频率: 如果系统检测到你的静默推送太频繁,会开始丢弃推送。
优化: 静默推送只用于"有重要数据需要预加载"的场景,不要当成轮询的替代品。
3.8 传感器
| 传感器 | 耗电 | 优化 |
|---|---|---|
| 加速度计 | 低 | 用 CMMotionManager 的合理更新频率,不用就 stop |
| 陀螺仪 | 中 | 同上 |
| 磁力计(指南针) | 中 | 用 headingFilter 过滤微小变化 |
| 气压计 | 低 | 按需使用 |
| 摄像头 | 极高 | 分辨率调到够用即可,不用就释放 |
| 麦克风 | 高 | 用 VAD(语音活动检测)避免持续录音 |
四、电量监控与测量
4.1 开发阶段
Instruments - Energy Log
Xcode 的 Instruments 提供 Energy Log 模板,能看到:
- CPU 活动(Overhead 级别:0-20)
- 网络活动
- 定位活动
- GPU 活动
- 前台/后台状态
每个指标用 0-20 的等级表示功耗水平。
Xcode Energy Gauges
Debug Navigator 里实时显示 Energy Impact(低/中/高/极高),直观但粗略。
Energy Impact 的颜色含义:
- 绿色(低):正常
- 黄色(中):有优化空间
- 红色(高/极高):需要关注
sysdiagnose
在设备上触发 sysdiagnose(同时按 音量上 + 音量下 + 电源键),生成一份详细的系统诊断报告,包含详细的电量日志。
4.2 线上监控
MetricKit(iOS 13+)
苹果提供的官方线上性能监控框架,每 24 小时汇总一次数据:
| Metric | 说明 |
|---|---|
MXCPUMetric |
CPU 使用指令数 |
MXGPUMetric |
GPU 使用时间 |
MXNetworkTransferMetric |
网络传输量(上/下行) |
MXLocationActivityMetric |
定位活动时间 |
MXCellularConditionMetric |
蜂窝信号质量(信号差时更耗电) |
MXAppRunTimeMetric |
前台/后台运行时间 |
这些数据以直方图形式提供,包含 P50/P90/P99 分位值。 可以帮你了解真实用户的耗电情况。
Xcode Organizer - Energy Reports
Xcode → Window → Organizer → Energy,可以看到线上用户的能量报告。如果你的 App 被系统判定为"耗电异常",这里会有日志。
4.3 电量归因:到底是谁在耗电?
当发现 App 耗电高时,排查思路:
markdown
1. CPU 高?
└── 用 Time Profiler 找到热点函数
└── 是主线程还是子线程?
└── 是否有不必要的循环/计算?
└── 后台是否有 Timer 在跑?
2. 网络频繁?
└── 用 Network instrument 看请求频率和数据量
└── 是否有轮询?
└── 请求是否可以合并?
└── 是否在蜂窝网络下做了大量传输?
3. 定位一直开着?
└── 检查 CLLocationManager 的 start/stop 配对
└── 精度是否过高?
└── 后台是否还在定位?
4. GPU 负载高?
└── 用 Core Animation instrument 检查离屏渲染
└── 是否有不必要的透明度混合?
└── 帧率是否过高?
五、Low Power Mode(低电量模式)适配
用户开启低电量模式后,系统会:
- 降低 CPU/GPU 频率
- 减少后台活动
- 降低屏幕亮度
- 关闭 5G(降回 4G)
- 停止自动下载和邮件获取
你的 App 应该监听并适配:
| 检测方式 | 说明 |
|---|---|
ProcessInfo.processInfo.isLowPowerModeEnabled |
查询当前状态 |
NSProcessInfoPowerStateDidChangeNotification |
监听状态变化 |
适配建议:
- 低电量模式下降低动画帧率或关闭动画
- 停止非关键的后台数据同步
- 降低定位精度
- 减少网络请求频率
- 延迟非紧急的计算任务
六、优化原则总结
六字箴言:少做、晚做、批量做
| 原则 | 含义 | 例子 |
|---|---|---|
| 少做 | 能不做就不做 | 不需要的数据不请求,不在屏的 View 不渲染 |
| 晚做 | 能推迟就推迟 | 非关键 SDK 延迟初始化,后台任务等充电时做 |
| 批量做 | 能合并就合并 | 网络请求合并,埋点批量上报 |
优化优先级
按耗电影响从大到小:
markdown
1. 🔴 网络(尤其是蜂窝网络的拖尾效应)
2. 🔴 定位(GPS 持续开启)
3. 🟡 CPU(后台 Timer、忙等待、过度计算)
4. 🟡 GPU(离屏渲染、高帧率)
5. 🟢 蓝牙(持续扫描)
6. 🟢 传感器(持续采集)
一张检查清单
□ Timer 是否设置了 tolerance?
□ 切后台后是否停止了不必要的 Timer / 定位 / 蓝牙扫描?
□ 网络请求是否有合并?是否有缓存?
□ 定位精度是否是最低够用的?用完是否关闭了?
□ 是否有轮询可以用推送替代?
□ 后台任务是否用了 BGTaskScheduler 而不是自己计时?
□ 图片是否压缩到了合适的尺寸?
□ 是否适配了低电量模式?
□ 大型计算任务是否标记了合适的 QoS?
□ 是否用 MetricKit 监控了线上耗电数据?