首页
/ Expo Battery 模块演进与实战:基于 expo-battery CHANGELOG 的版本历史、API 与底层实现解析

Expo Battery 模块演进与实战:基于 expo-battery CHANGELOG 的版本历史、API 与底层实现解析

2026-09-08 14:56:32作者:羿妍玫Ivan

本篇围绕 Expo 官方仓库中 packages/expo-battery 模块的变更记录展开,梳理其从 2020 年以来的关键版本演进,并结合 Android(Kotlin)、iOS(Swift)与 Web 端源码,深入剖析电量 API、事件订阅机制与跨平台状态映射的实现原理。读完本文,你将理解 expo-battery 的完整 API 能力、各版本破坏性变更的来龙去脉,以及如何在自己的 Expo 项目中正确读取与监听设备电量状态。

模块概览:定位与能力边界

expo-battery 是 Expo SDK 中提供物理设备电池信息的官方模块,其设计目标在 README.md 中表述为一句非常简洁的话:Provide battery information for the physical device.。它不负责"省电优化"或"电量管理",而是一个纯粹的只读信息源 + 事件源

package.json 可以看到,当前仓库内的模块版本为 57.0.1,零运行时依赖("dependencies": {}),仅以 exporeact 作为 peerDependencies。TypeScript 类型入口为 build/Battery.d.ts,运行入口为 build/Battery.js,源码则全部位于 src 目录。

在支持范围上,模块的能力边界在 isAvailableAsync 的实现注释 中有明确交代:

  • Android 真机isSupported 恒为 true(对应 BatteryModule.ktConstant("isSupported") { true });
  • iOS 真机:支持;iOS 模拟器返回 falseBatteryModule.swift#if targetEnvironment(simulator) 编译期区分,因为模拟器没有真实电池);
  • Web:取决于浏览器是否实现了 Web Battery API(navigator.getBattery,见 ExpoBattery.web.ts)。

expo-battery 是典型的 "Expo Module(本地原生模块)" 结构,由 ExpoBattery.ts 中的 requireNativeModule('ExpoBattery') 在原生侧加载名为 ExpoBattery 的模块(Android 与 iOS 的 Name("ExpoBattery") 与此对应),Web 端则通过 package.json / 构建系统自动路由到 ExpoBattery.web.ts 的纯 JS 实现。

API 全景:四个异步查询方法

无论版本如何演进,expo-battery 面向开发者暴露的异步查询 API 保持稳定,全部定义在 Battery.ts 中。这些方法均带有一致的设计约定:当原生模块不支持对应能力时返回哨兵值(-1 / false / UNKNOWN),而不是抛错,因此上层调用无需逐个 try-catch。

getBatteryLevelAsync:读取电量百分比

返回 01(含)之间的数值,代表剩余电量比例;若设备无法提供则返回 -1源码):

await Battery.getBatteryLevelAsync();
// 0.759999

底层实现随平台不同而有显著差异:

  • AndroidBatteryModule.kt 通过注册一个 null receiver 来获取粘性广播 ACTION_BATTERY_CHANGED,再从 Intent 中取 EXTRA_LEVELEXTRA_SCALE,两者相除得到比例。当任一 extra 缺失(值为 -1)时返回 -1f
  • iOS:直接读取 UIDevice.current.batteryLevelBatteryModule.swift)。需要留意的是,iOS 依赖 UIDevice.current.isBatteryMonitoringEnabled = true 才会开始回报真实电量,模块在 OnCreate 开启、OnDestroy 关闭该开关。
  • Web:返回 BatteryManager.level;且按 Battery.ts 文档所述,"On web, this method always returns 1" 是文档中针对旧版行为的描述,实际现代实现见 ExpoBattery.web.ts,在拿不到 BatteryManager 时返回 -1

getBatteryStateAsync:读取充电状态枚举

返回 BatteryState 枚举值。Battery.types.ts 定义的枚举共五态:

枚举值 数值 含义
UNKNOWN 0 状态未知或不可访问
UNPLUGGED 1 放电中(典型为未接电源)
CHARGING 2 正在充电
FULL 3 电量已满
NOT_CHARGING 4 已接入电源但未充电(如充电保护限制在 80%),仅 Android 平台会出现

该枚举在 Android 原生侧(BatteryModule.ktBatteryState 嵌套枚举)与 JS 侧数值一一对应,确保跨层传输时语义一致。

isLowPowerModeEnabledAsync:低电量模式检测

Android 对应"节电模式(Power Saver Mode)",iOS 对应"低电量模式(Low Power Mode)"。若平台不支持上报(如 Web、部分旧 Android 设备),恒返回 false源码)。

isBatteryOptimizationEnabledAsync:电池优化白名单检测(Android 6.0+)

该方法自 5.0.0 版本引入(见下文版本史)。它检查本应用是否被系统做了电池优化——若被优化,应用进入 Doze 模式后后台任务可能被影响。实现见 BatteryModule.kt

val powerManager = context.getSystemService(Context.POWER_SERVICE) as? PowerManager
return powerManager?.isIgnoringBatteryOptimizations(packageName) == false

逻辑上,isIgnoringBatteryOptimizations 返回 false 意味着应用未豁免优化,即"优化已启用",因此方法返回 true。iOS 与 Web 无此概念,JS 侧对不存在的原生方法统一回退为 false

聚合查询与 React Hooks:getPowerStateAsync 与四个 Hook

getPowerStateAsync:一次拿到全部

getPowerStateAsync 内部用 Promise.all 并行聚合上述三个查询,返回 PowerState 对象(源码),字段定义见 Battery.types.ts

await Battery.getPowerStateAsync();
// {
//   batteryLevel: 0.759999,
//   batteryState: BatteryState.UNPLUGGED,
//   lowPowerMode: true,
// }

与手动逐个查询不同,该聚合方法会重新抛出任一子查询中的错误(而不是吞掉返回哨兵值),文档明确说明 "This method re-throws any errors",适用于希望严格失败语义的场景。

React Hooks:useBatteryLevel / useBatteryState / useLowPowerMode / usePowerState

这组 Hook 由 7.3.0 版本(2023-06-13)新增。从源码看,它们的实现模式高度统一:挂载时先调用对应异步查询填充初值,随后订阅变更事件,卸载时移除监听。以 useBatteryLevel 为例:

export function useBatteryLevel(): number {
  const [batteryLevel, setBatteryLevel] = useState(-1);

  useEffect(() => {
    getBatteryLevelAsync().then(setBatteryLevel);
    const listener = addBatteryLevelListener((b) => setBatteryLevel(b.batteryLevel));
    return () => listener.remove();
  }, []);

  return batteryLevel;
}

四个 Hook 的初始值约定为:电量 -1、状态 UNKNOWN、低电量模式 false。其中 usePowerState 内部订阅了全部三个事件并在清理函数中逐个 remove(),这正是"订阅生命周期管理"的标准范式。

事件订阅机制:三个监听方法与一次 iOS 修复

addBatteryLevelListener / addBatteryStateListener / addLowPowerModeListener

模块通过 Expo 事件发射器暴露三个原生事件(事件名常量):

  • Expo.batteryLevelDidChange:电量变化
  • Expo.batteryStateDidChange:充电状态变化
  • Expo.powerModeDidChange:低电量模式开关变化

事件语义存在显著平台差异,源码文档 记载如下:

  • AndroidbatteryLevelDidChange 只在显著变化时触发,即电量跌破 ACTION_BATTERY_LOW 阈值或从低电量回升越过 ACTION_BATTERY_OKAY。对应地,Android 侧为电量监听单独注册了带这两个 action 的 IntentFilterBatteryModule.kt),而充电状态监听(BatteryStateReceiver)订阅的是 ACTION_BATTERY_CHANGED,低电量模式监听(PowerSaverReceiver)订阅的是 android.os.action.POWER_SAVE_MODE_CHANGED
  • iOS:电量事件在电量下降 1% 及以上时触发,但每分钟最多触发一次
  • WebaddBatteryLevelListener / addBatteryStateListener 事件绑定的是 BatteryManager 的 levelchangechargingchange 浏览器事件(ExpoBattery.web.ts)。此外 Web 端对状态变更做了去重emitStateChange 会记录 lastReportedState,避免重复派发相同状态。

事件回调事件类型:5.0.0 的 API 破坏点

5.0.0(2021-06-16)是事件 API 的重要分水岭:移除了 BatteryLevelUpdateListenerBatteryStateUpdateListenerPowerModeUpdateListener 三个"仅包装单参数事件响应"的监听器类型,要求开发者直接使用显式事件类型 BatteryLevelEventBatteryStateEventPowerModeEvent。这三者的字段定义在 Battery.types.ts,分别携带 batteryLevelbatteryStatelowPowerMode 键,与你从事件对象中取出的内容一一对应。

Unpublished 变更:#48377 将 iOS 观察者按事件隔离

在 CHANGELOG 的 "Unpublished"(未发布)区段中,有一条值得关注的 iOS 缺陷修复:

[iOS] Scope each notification observer to its own event so subscribing to one battery event no longer starts observing the others.(expo/expo#48377)

结合 BatteryModule.swift 的现状可以理解该问题:当前实现为每个事件分别配置 OnStartObserving(...) / OnStopObserving(...),各自向 NotificationCenter 注册对应的系统通知:

事件 监听的系统通知
batteryLevelDidChange UIDevice.batteryLevelDidChangeNotification
batteryStateDidChange UIDevice.batteryStateDidChangeNotification
powerModeDidChange NSProcessInfoPowerStateDidChange

该修复进一步收紧了事件隔离粒度,确保订阅某个电池事件(如仅监听电量)不再连带启动其余两个观察者,从而减少无谓的原生回调开销——这在仅需单一事件的低功耗场景中尤为关键。

平台兼容性变更与跨平台语义修正

CHANGELOG 中最能体现工程严谨性的,是历次对最低系统版本状态映射语义的调整。这些变更直接影响你项目的兼容边界与判断逻辑。

支持的最低系统版本演进

从 CHANGELOG 可梳理出模块的最低部署目标变化:

版本 变更 影响平台
9.0.0(2024-10-22) iOS deployment target 提升至 15.1 iOS
7.7.0(2023-11-14) iOS 提升至 13.4;Android compileSdkVersion/targetSdkVersion 升至 34 iOS / Android
7.6.0(2023-10-17) 放弃 Android SDK 21、22 支持 Android
7.0.0(2022-10-25) iOS 提升至 13.0,废弃 iOS 12 iOS
6.0.0(2021-09-28) 放弃 iOS 11.0 支持 iOS
4.0.0(2021-01-15) 放弃 iOS 10.0 支持 iOS
56.0.0(2026-05-05) iOS/tvOS 最低 16.4,macOS 最低 13.4 Apple 全系

结合当前 expo-battery/ios 目录中 ExpoBattery.podspec 所在位置可见,模块沿用 Expo Modules 的统一工程配置;上述 Apple 平台门槛的大幅提升(56.0.0)与 Expo SDK 整体基线同步。Android 侧在 3.0.0 时代即曾出现 compileSdkVersiontargetSdkVersion 随 SDK 周期滚动升级的记录(如 6.2.0 升至 31、7.1.0 升至 33),这也解释了为何新版本对较旧 RN 工程有隐性构建要求。

Android NOT_CHARGING:充电保护场景下的状态映射修复(56.0.0)

56.0.0(2026-05-05)修复了一个直接影响状态判断正确性的缺陷:

[android] Add BatteryState.NOT_CHARGING when power is connected but the battery is not charging (e.g. battery protection); map BATTERY_STATUS_DISCHARGING to UNPLUGGED only.

该修复的落地实现,正是 Android 侧的核心映射函数 batteryStatusNativeToJS

fun batteryStatusNativeToJS(status: Int, plugged: Int): BatteryModule.BatteryState {
  return when (status) {
    BatteryManager.BATTERY_STATUS_FULL -> BatteryModule.BatteryState.FULL
    BatteryManager.BATTERY_STATUS_CHARGING -> BatteryModule.BatteryState.CHARGING
    BatteryManager.BATTERY_STATUS_NOT_CHARGING ->
      if (plugged != 0) {
        BatteryModule.BatteryState.NOT_CHARGING
      } else {
        BatteryModule.BatteryState.UNPLUGGED
      }
    BatteryManager.BATTERY_STATUS_DISCHARGING -> BatteryModule.BatteryState.UNPLUGGED
    else -> BatteryModule.BatteryState.UNKNOWN
  }
}

修复前,Android 的 BATTERY_STATUS_NOT_CHARGING 场景——例如插着电源但电池保护功能把充电停在 80%、或优化充电暂停——会被直接归类为 UNPLUGGED,造成"明明连着电源却显示在放电"的语义混乱。修复后:

  • 系统回报 BATTERY_STATUS_NOT_CHARGINGEXTRA_PLUGGED != 0(有电源接入)时 → 映射为新增的 NOT_CHARGING(值 4);
  • BATTERY_STATUS_DISCHARGING 成为唯一通向 UNPLUGGED 的路径;
  • 没有电源但系统仍报 NOT_CHARGING 的边界情况,则兜底归为 UNPLUGGED

JS 侧 BatteryState 枚举 同步补充了 NOT_CHARGING,并标注"On iOS and web, this value is never returned"。iOS 之所以天然不会出现该值,是因为 BatteryModule.swift 直接返回 UIDevice.current.batteryState.rawValue,而 UIKit 的 UIDevice.BatteryState 枚举只有 unknown/unplugged/charging/full 四态,恰好与 Expo 枚举前四项一一对应——这也是为何 iOS 实现敢于用注释写明 "Apple's enum values directly correspond to Expo's" 直接透传的原因。

56.0.4:清理 Android 端未使用的原生依赖

56.0.4(2026-05-19)记录了一条 Android 清理类改动:"Remove unused native dependencies."。这类变更通常不会改变 API 行为,但会缩小模块的依赖面、减少二进制体积与潜在的依赖冲突面,属于典型的发布前健康检查结果。

模块工程化演进:从旧架构到 Expo Modules API

CHANGELOG 中散落的工程化改动,串起来正是一条 Expo 原生模块基础设施的演进主线,可作为理解 Expo 模块开发现状的生动案例:

原生语言迁移:Java → Kotlin → Swift

  • 6.0.0(2021-09-28):Android 实现由 Java 重写为 Kotlin("Rewrite BatteryModule from Java to Kotlin"),并同期补充了单元测试("Add unit tests")。这正好与 getBatteryStateAsync 的状态映射逻辑需要精确验证相呼应——映射函数 batteryStatusNativeToJS 被设计为无状态的纯函数,天然便于单测覆盖(从当前 Kotlin 文件结构看,该映射被独立抽到 BatteryStatusNativeToJS.kt,正是为可测试性服务的分层设计)。
  • 6.0.0 同时完成从 @unimodules/coreexpo-modules-core 的迁移,标志着模块接入新一代模块内核。
  • 7.0.0(2022-10-25):iOS 原生模块改用 Swift + Sweet API 编写,即当前 BatteryModule.swift 中基于 ModuleDefinition 的声明式定义风格。

构建体系演进

  • 8.0.0(2024-04-18):移除废弃的向后兼容 Gradle 设置,并删除了 Web 端不再使用的 name 属性。
  • 9.1.0(2025-04-04):Android 开始使用 expo modules Gradle 插件;Apple 侧把剩余 expo-module.config.json 迁移到统一平台语法。这两步都属于 Expo Modules 自动链接(autolinking)体系的标准收敛。

版本号机制:从 7.x → 8/9/10 → 55+ 的跃迁

观察 CHANGELOG 的版本序列会发现:2025 年 8 月发布的 10.0.0 之后,下一版本直接跳到了 55.0.0(2026-01-21)——这是因为 Expo SDK 自 SDK 55 起将各模块版本号与 SDK 主版本统一对齐,后续 56.x、57.x 均沿此规则。在这条序列中,大量版本标注为 "This version does not introduce any user-facing changes.",即纯内部发布(依赖对齐、构建配置微调、机器人发版),这是大型 monorepo 中常见的现象:每个 SDK 版本都要重新发版所有模块,即便某个模块本身没有任何面向用户的改动。

从 CHANGELOG 看出的实践要点

将上述版本历史压缩为可操作的结论,可以提炼出几条对使用者的建议:

  1. 判断状态时务必考虑 Android 的 NOT_CHARGING:自 SDK 56(56.0.0)起,接电源但未充电(电池保护、充电暂停)会返回 NOT_CHARGING 而非 UNPLUGGED。如果你的 UI 用 UNPLUGGED 来渲染"未充电"提示,需按需扩展判断;若仍需兼容旧 SDK,可同时接受这两个值。
  2. 事件订阅是廉价的,但要按需订阅:iOS 侧自未发布版 #48377 起已将每个事件观察者隔离,单个事件的订阅不再拖拽其他观察者启动。Android 侧每个 Receiver 只在有监听需要时才注册(模块在 Activity 进入前台时注册、退后台时注销,见 BatteryModule.kt),整体遵循"按需观察"原则。React Hook 内部会自动管理订阅的注册与清理,手动订阅时务必记得调用返回的 Subscription.remove()
  3. 不同平台的事件粒度不可互换:Android 的电量事件是"低电量/恢复"级别的粗粒度广播,iOS 是"下降≥1%但每分钟至多一次",Web 则是浏览器事件驱动。做跨平台一致体验时,轮询 + 事件混合策略往往比依赖单一事件源更可靠。
  4. 安装与版本匹配:在 Expo 托管工程中按官方推荐安装即可与 SDK 自动对齐:
npx expo install expo-battery

裸 RN 工程则需先完成 expo 包 的基础安装与配置,再执行上述命令(详见 README)。

延伸阅读路径

如果你希望进一步深入 expo-battery 的实现细节,可以按以下仓库路径继续追踪:

以上文件共同构成了一个麻雀虽小、五脏俱全的 Expo 模块范例——从双端原生实现、Web 降级、统一类型定义到严谨的单元测试与逐版本语义修正,足以作为学习 Expo Modules 架构的参考样板。

热门项目推荐
相关项目推荐

项目优选

收起
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.15 K
2.77 K
kernelkernel
deepin linux kernel
C
34
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
929
1.85 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.36 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.94 K
1.03 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.47 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
534
603
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
398
leetcodeleetcode
🔥LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer(第 2 版)》、《程序员面试金典(第 6 版)》题解
Markdown
77
23