Expo Battery 模块演进与实战:基于 expo-battery CHANGELOG 的版本历史、API 与底层实现解析
本篇围绕 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": {}),仅以 expo 与 react 作为 peerDependencies。TypeScript 类型入口为 build/Battery.d.ts,运行入口为 build/Battery.js,源码则全部位于 src 目录。
在支持范围上,模块的能力边界在 isAvailableAsync 的实现注释 中有明确交代:
- Android 真机:
isSupported恒为true(对应 BatteryModule.kt 中Constant("isSupported") { true }); - iOS 真机:支持;iOS 模拟器返回
false(BatteryModule.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:读取电量百分比
返回 0 到 1(含)之间的数值,代表剩余电量比例;若设备无法提供则返回 -1(源码):
await Battery.getBatteryLevelAsync();
// 0.759999
底层实现随平台不同而有显著差异:
- Android:BatteryModule.kt 通过注册一个
nullreceiver 来获取粘性广播ACTION_BATTERY_CHANGED,再从 Intent 中取EXTRA_LEVEL与EXTRA_SCALE,两者相除得到比例。当任一 extra 缺失(值为-1)时返回-1f。 - iOS:直接读取
UIDevice.current.batteryLevel(BatteryModule.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.kt 的 BatteryState 嵌套枚举)与 JS 侧数值一一对应,确保跨层传输时语义一致。
isLowPowerModeEnabledAsync:低电量模式检测
Android 对应"节电模式(Power Saver Mode)",iOS 对应"低电量模式(Low Power Mode)"。若平台不支持上报(如 Web、部分旧 Android 设备),恒返回 false(源码)。
- Android 端读取
PowerManager.isPowerSaveMode(BatteryModule.kt); - iOS 端读取
ProcessInfo.processInfo.isLowPowerModeEnabled(BatteryModule.swift)。
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:低电量模式开关变化
事件语义存在显著平台差异,源码文档 记载如下:
- Android:
batteryLevelDidChange只在显著变化时触发,即电量跌破ACTION_BATTERY_LOW阈值或从低电量回升越过ACTION_BATTERY_OKAY。对应地,Android 侧为电量监听单独注册了带这两个 action 的IntentFilter(BatteryModule.kt),而充电状态监听(BatteryStateReceiver)订阅的是ACTION_BATTERY_CHANGED,低电量模式监听(PowerSaverReceiver)订阅的是android.os.action.POWER_SAVE_MODE_CHANGED。 - iOS:电量事件在电量下降 1% 及以上时触发,但每分钟最多触发一次。
- Web:
addBatteryLevelListener/addBatteryStateListener事件绑定的是 BatteryManager 的levelchange与chargingchange浏览器事件(ExpoBattery.web.ts)。此外 Web 端对状态变更做了去重:emitStateChange会记录lastReportedState,避免重复派发相同状态。
事件回调事件类型:5.0.0 的 API 破坏点
5.0.0(2021-06-16)是事件 API 的重要分水岭:移除了 BatteryLevelUpdateListener、BatteryStateUpdateListener、PowerModeUpdateListener 三个"仅包装单参数事件响应"的监听器类型,要求开发者直接使用显式事件类型 BatteryLevelEvent、BatteryStateEvent、PowerModeEvent。这三者的字段定义在 Battery.types.ts,分别携带 batteryLevel、batteryState、lowPowerMode 键,与你从事件对象中取出的内容一一对应。
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 时代即曾出现 compileSdkVersion、targetSdkVersion 随 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_CHARGINGwhen power is connected but the battery is not charging (e.g. battery protection); mapBATTERY_STATUS_DISCHARGINGtoUNPLUGGEDonly.
该修复的落地实现,正是 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_CHARGING且EXTRA_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/core到expo-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 看出的实践要点
将上述版本历史压缩为可操作的结论,可以提炼出几条对使用者的建议:
- 判断状态时务必考虑 Android 的
NOT_CHARGING:自 SDK 56(56.0.0)起,接电源但未充电(电池保护、充电暂停)会返回NOT_CHARGING而非UNPLUGGED。如果你的 UI 用UNPLUGGED来渲染"未充电"提示,需按需扩展判断;若仍需兼容旧 SDK,可同时接受这两个值。 - 事件订阅是廉价的,但要按需订阅:iOS 侧自未发布版 #48377 起已将每个事件观察者隔离,单个事件的订阅不再拖拽其他观察者启动。Android 侧每个 Receiver 只在有监听需要时才注册(模块在 Activity 进入前台时注册、退后台时注销,见 BatteryModule.kt),整体遵循"按需观察"原则。React Hook 内部会自动管理订阅的注册与清理,手动订阅时务必记得调用返回的
Subscription.remove()。 - 不同平台的事件粒度不可互换:Android 的电量事件是"低电量/恢复"级别的粗粒度广播,iOS 是"下降≥1%但每分钟至多一次",Web 则是浏览器事件驱动。做跨平台一致体验时,轮询 + 事件混合策略往往比依赖单一事件源更可靠。
- 安装与版本匹配:在 Expo 托管工程中按官方推荐安装即可与 SDK 自动对齐:
npx expo install expo-battery
裸 RN 工程则需先完成 expo 包 的基础安装与配置,再执行上述命令(详见 README)。
延伸阅读路径
如果你希望进一步深入 expo-battery 的实现细节,可以按以下仓库路径继续追踪:
- JS API 与 Hook 的完整实现:src/Battery.ts
- 类型、枚举与事件映射定义:src/Battery.types.ts
- Web 端实现(BatteryManager 封装):src/ExpoBattery.web.ts
- Android 模块入口与广播接收器注册:BatteryModule.kt
- Android 原生状态 → JS 状态映射函数:BatteryStatusNativeToJS.kt
- iOS Swift 实现与通知中心观察者管理:BatteryModule.swift
- 模块包配置与脚本:package.json
- 完整变更记录(本文的事实来源):CHANGELOG.md
以上文件共同构成了一个麻雀虽小、五脏俱全的 Expo 模块范例——从双端原生实现、Web 降级、统一类型定义到严谨的单元测试与逐版本语义修正,足以作为学习 Expo Modules 架构的参考样板。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.21 K635- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python70
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java201
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java90
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript120
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python300