macOS上的电源数据并不像电池图标那样只有一个百分比,系统底层把交流电源、内置电池、UPS等统一抽象为电源源对象。Power Box这类工具要做的第一件事,就是通过IOKit的电源服务接口拿到这些对象,并从中读取当前容量、最大容量、充电状态、剩余时间等字段。这个过程和直接读取系统偏好设置不一样,它依赖Core Foundation风格的对象与字典,因此内存管理方式也需要按照CF规则处理。

一、通过IOKit枚举电池并读取容量信息
IOKit在电源管理部分提供了一组以IOPS开头的C函数。获取电池信息的第一步是调用IOPSCopyPowerSourcesInfo,它会返回一个不透明的CFTypeRef对象,表示当前系统所有电源源的信息快照。接着使用IOPSCopyPowerSourcesList把快照展开成数组,数组中的每一项都是一个电源源引用。最后通过IOPSGetPowerSourceDescription拿到描述字典,字典中包含电池名称、当前容量、最大容量、充电状态等键值对。
下面的代码演示了完整的读取流程。这里使用kIOPSCurrentCapacityKey和kIOPSMaxCapacityKey计算电池百分比,同时读取kIOPSPowerSourceStateKey判断当前由电池供电还是交流电供电。
// 获取电池信息并计算电量百分比
CFTypeRef powerInfo = IOPSCopyPowerSourcesInfo();
if (powerInfo == NULL) {
NSLog(@"无法创建电源信息快照");
return;
}
CFArrayRef powerSources = IOPSCopyPowerSourcesList(powerInfo);
if (powerSources == NULL || CFArrayGetCount(powerSources) == 0) {
if (powerSources != NULL) CFRelease(powerSources);
CFRelease(powerInfo);
return;
}
for (CFIndex i = 0; i < CFArrayGetCount(powerSources); i++) {
CFTypeRef source = CFArrayGetValueAtIndex(powerSources, i);
CFDictionaryRef description = IOPSGetPowerSourceDescription(powerInfo, source);
if (description == NULL) continue;
NSString *name = (__bridge NSString *)CFDictionaryGetValue(description, CFSTR(kIOPSNameKey));
NSNumber *currentCapacity = (__bridge NSNumber *)CFDictionaryGetValue(description, CFSTR(kIOPSCurrentCapacityKey));
NSNumber *maxCapacity = (__bridge NSNumber *)CFDictionaryGetValue(description, CFSTR(kIOPSMaxCapacityKey));
NSString *powerState = (__bridge NSString *)CFDictionaryGetValue(description, CFSTR(kIOPSPowerSourceStateKey));
if (name != nil && currentCapacity != nil && maxCapacity != nil && [maxCapacity intValue] > 0) {
double percent = [currentCapacity doubleValue] / [maxCapacity doubleValue] * 100.0;
NSLog(@"电源源:%@,电量:%.1f%%,状态:%@", name, percent, powerState);
}
NSString *isCharging = (__bridge NSString *)CFDictionaryGetValue(description, CFSTR(kIOPSIsChargingKey));
if (isCharging != nil) {
NSLog(@"是否正在充电:%@", isCharging);
}
}
CFRelease(powerSources);
CFRelease(powerInfo);
这段代码里有两个容易忽略的地方。第一,IOPSCopyPowerSourcesInfo和IOPSCopyPowerSourcesList返回的对象都由调用方负责释放,即使中途某个字典读取失败,也要确保CFRelease最终执行。第二,字典中的值多数是CFNumberRef或CFStringRef,通过toll-free bridging可以直接转换为NSNumber和NSString,但转换后不能修改这些对象,因为它们仍然由字典持有。
状态判断通常会比较kIOPSPowerSourceStateKey的值是否为CFSTR(kIOPSBatteryPowerValue)或CFSTR(kIOPSACPowerValue)。如果系统连接了外部电池或者UPS,数组里可能同时出现多个电源源,此时需要结合kIOPSTransportTypeKey或名称区分内置电池。例如内置电池的传输类型通常为Internal,而UPS可能是USB或Serial。
二、注册电源状态变化的回调节点
只读取一次电池信息并不够,Power Box需要在用户插拔电源、电池开始充电或停止充电时及时更新界面。IOKit为此提供了IOPSNotificationCreateRunLoopSource,它创建一个CFRunLoopSource,当电源源变化时触发回调。这个API不是传统意义上的通知中心,而是把事件源直接挂到指定的RunLoop上,因此必须在使用线程的RunLoop中主动添加。
下面创建一个电源变化监听器,回调函数中重新执行电量读取逻辑,并刷新界面。注意回调函数运行在RunLoop所在线程,如果该线程不是主线程,更新UI前需要切回主队列。
// 全局引用,用于移除监听
static CFRunLoopSourceRef powerSourceRunLoopSource = NULL;
static void PowerSourceChangedCallback(void *context) {
NSLog(@"电源状态可能发生变化,重新读取电池信息");
// 这里调用前面定义的读取电量函数
dispatch_async(dispatch_get_main_queue(), ^{
// 更新Power Box界面
});
}
- (void)startWatchingPowerSourceChanges {
CFRunLoopSourceRef source = IOPSNotificationCreateRunLoopSource(PowerSourceChangedCallback, (__bridge void *)self);
if (source == NULL) {
NSLog(@"创建电源通知RunLoopSource失败");
return;
}
powerSourceRunLoopSource = source;
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
}
- (void)stopWatchingPowerSourceChanges {
if (powerSourceRunLoopSource != NULL) {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), powerSourceRunLoopSource, kCFRunLoopDefaultMode);
CFRelease(powerSourceRunLoopSource);
powerSourceRunLoopSource = NULL;
}
}
实际使用时,如果startWatchingPowerSourceChanges在主线程调用,就保持回调在主线程;如果在后台队列调用,需要单独维护一个线程或RunLoop,否则回调可能随线程死亡而失效。对于Power Box这类有常驻状态栏的应用,最简单的做法是在主线程启动监听,保证RunLoop长期存在。
还有一个需要区分的概念是IORegisterForSystemPower。它监听的是系统级电源事件,比如休眠、唤醒、关机,而不是电池和交流电的切换。两者的回调参数和触发场景完全不同。如果只想感知适配器插入与拔出,就不要使用IORegisterForSystemPower,否则可能收不到电池切换事件,还可能错误处理系统睡眠流程。
三、使用NSProcessInfo监听低电量模式
低电量模式是macOS在较新版本中引入的节能能力,开启后系统会降低后台活动、减少视觉动画,并限制部分硬件性能。Power Box需要实时展示这个状态,可以通过NSProcessInfo读取。每个进程都有processInfo单例,直接访问它的isLowPowerModeEnabled属性就能拿到当前布尔值,不需要向IOKit发送额外请求。
当低电量模式开启或关闭时,系统会发送NSProcessInfoPowerStateDidChangeNotification通知。监听这个通知比轮询属性更省资源,也更能及时反映系统变化。下面的代码展示了通知注册和回调处理方式。
- (void)startWatchingLowPowerMode {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(lowPowerModeDidChange:)
name:NSProcessInfoPowerStateDidChangeNotification
object:nil];
}
- (void)lowPowerModeDidChange:(NSNotification *)notification {
BOOL isLowPower = [NSProcessInfo processInfo].isLowPowerModeEnabled;
NSLog(@"低电量模式:%@", isLowPower ? @"开启" : @"关闭");
dispatch_async(dispatch_get_main_queue(), ^{
// 刷新Power Box界面
});
}
- (void)stopWatchingLowPowerMode {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:NSProcessInfoPowerStateDidChangeNotification
object:nil];
}
这个通知不仅覆盖低电量模式,也会在系统电源状态发生较大变化时发出。因此回调里不要直接假设一定是低电量模式切换,而是每次读取isLowPowerModeEnabled的当前值。如果应用需要最小支持版本早于低电量模式引入的版本,可以在运行时检查属性是否存在,或使用respondsToSelector:做兼容。
需要注意的是,macOS在虚拟机和某些外接显示器环境中可能不会主动触发低电量模式通知,手动切换节能设置也并非总是走通知路径。因此Power Box可以把通知监听和定时轮询结合,每30秒重新读取一次isLowPowerModeEnabled,防止界面状态长期不更新。
四、整合电源管理类并处理生命周期
把电池读取、电源变化监听和低电量模式监听拆成独立函数后,可以进一步封装成一个PowerBoxManager类。它对外暴露电池百分比、是否充电、是否使用电池供电、是否开启低电量模式等属性,并通过代理或Block通知外部刷新。类的内部维护一个电源信息快照和监听器,避免每个页面重复调用Core Foundation函数。
下面给出一个简化版的管理类接口,展示如何组合这些能力。初始化时读取一次电池信息,然后启动两个监听。外部只要设置代理,不需要关心底层细节。
@interface PowerBoxManager : NSObject
@property (nonatomic, assign) double batteryPercent;
@property (nonatomic, assign) BOOL isCharging;
@property (nonatomic, assign) BOOL isOnBatteryPower;
@property (nonatomic, assign) BOOL isLowPowerModeEnabled;
- (void)start;
- (void)stop;
@end
@implementation PowerBoxManager
- (void)start {
[self refreshBatteryInfo];
[self startWatchingPowerSourceChanges];
[self startWatchingLowPowerMode];
}
- (void)stop {
[self stopWatchingPowerSourceChanges];
[self stopWatchingLowPowerMode];
}
- (void)refreshBatteryInfo {
// 调用前面的电池信息读取函数,并更新属性
}
@end
管理类的stop方法务必移除之前添加的RunLoopSource和通知观察者。Core Foundation对象不会像ARC那样自动释放,如果只创建不释放,每次启动监听都会泄漏一个CFRunLoopSourceRef。通知观察者如果不移除,在对象释放之后系统仍然可能向野指针发送消息,导致闪退。这在常驻型工具中尤其重要,因为运行时间越长,问题越容易被放大。
测试Power Box时,建议覆盖三个典型场景:拔下电源适配器、重新插入适配器、在节能设置中反复开关低电量模式。正常情况下,前两个场景会触发IOKit的电源源变化回调,第三个场景会触发NSProcessInfo通知。如果回调没有及时到达,可以检查RunLoop是否正常运行、通知观察者是否还存活,以及应用是否在后台被系统挂起。最终保持代码结构清晰,每个监听模块独立启停,就能让macOS电源信息采集功能稳定工作。