导读:本期聚焦于芒果创作的《macOS Power Box开发中如何用IOKit读取电池信息并监听低电量模式通知?》,敬请观看详情。IOKit在macOS上直接连接内核电源管理子系统,能够拿到电池容量、充电状态、电源来源等底层数据。本文围绕一个Power Box工具的构建过程,说明如何调用IOPSCopyPowerSourcesInfo枚举电池对象、从电源描述字典中提取当前电量和健康度字段,以及如何通过IOPSNotificationCreateRunLoopSource注册电源状态变化回调。文章还会讨论NSProcessInfo提供的低电量模式接口,包括isLowPowerModeEnabled属性与NSProcessInfoPowerStateDidChangeNotification通知的组合使用方式。代码采用Objective-C编写,覆盖CF对象内存管理、回调线程切换、通知移除等容易出错的细节,最后给出一个可维护的电源管理类结构建议。

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

macOS Power Box开发中如何用IOKit读取电池信息并监听低电量模式通知?

一、通过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电源信息采集功能稳定工作。

IOKit电池信息低电量模式修改时间:2026-09-20 10:08:41

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0920/59604.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。