# iOS 电量优化详解

一、电量是怎么被消耗的?

手机电池的电量本质上就是电能。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 环境下进行
  • NWPathMonitorReachability 判断当前网络类型

4. 减少数据传输量

  • 开启 HTTP 压缩(gzip / br)
  • 用 HTTP/2 的头部压缩
  • 图片用 WebP / HEIF 替代 PNG/JPEG
  • API 只返回需要的字段(GraphQL 的优势)
  • 合理使用缓存(URLCacheETagLast-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 监控了线上耗电数据?
相关推荐
忆江南5 小时前
# iOS weak 原理详解
前端
小码哥_常5 小时前
解锁Android开发封装密码,打造高效代码城堡
前端
在西安放羊的牛油果5 小时前
我把 2000 行下单代码,重构成了一套交易前端架构
前端·设计模式·架构
im_AMBER5 小时前
今日开发反思:编辑器大纲跳转与数据持久化实践
前端·架构
Qinana5 小时前
从数据包旅程到首屏渲染:深入理解 TCP/IP 如何决定你的 Web 性能
前端·tcp/ip·浏览器
橙子的AI笔记5 小时前
旧版 LangChain 已死:新版竟以LangGraph为底座封装
前端·langchain
Wect5 小时前
LeetCode 17. 电话号码的字母组合:回溯算法入门实战
前端·算法·typescript
SuperEugene5 小时前
Vue3 中后台实战:VXE-Table 从基础表格到复杂业务表格全攻略 | Vue生态精选篇
前端·javascript·vue.js
SuperEugene5 小时前
Vue3 中后台实战:Element + VXE Table 搜索表格分页完整方案 | Vue生态精选篇
前端·javascript·vue.js