You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于USBPrivateDataSample适配的IOKit代码内存泄漏排查

排查USB设备监听代码中的内存泄漏问题

嘿,我来帮你揪出这个内存泄漏的问题!先理清楚你的情况:你需要监听所有USB设备的连接/断开事件,之前适配HID的方案满足不了需求,于是基于苹果的USBPrivateDataSample和ORSSerialPort从零写了代码,功能正常但内存泄漏一直没解决,对吧?咱们一步步拆解问题。

首先先把你提供的代码整理清晰:

你的代码片段

AppDelegate.h

#import <Cocoa/Cocoa.h>

@interface AppDelegate : NSObject <NSApplicationDelegate>
@property (strong) NSMutableDictionary *deviceDictionary;
@end

AppDelegate.m(部分代码)

#import "AppDelegate.h"
#include <IOKit/usb/IOUSBLib.h>

typedef struct DeviceData {
    io_object_t notification;
    CFStringRef deviceName;
    CFStringRef serialNum;
} DeviceData;

static IONotificationPortRef gNotifyPort;
static io_iterator_t gAddedIter;
static CFRunLoopRef gRunLoop;

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    self.deviceDictionary = [[NSMutableDictionary alloc] init];
    
    CFMutableDictionaryRef matchingDict;
    CFRunLoopSourceRef runLoopSource;
    kern_return_t kr;
    
    matchingDict = IOServiceMatching(kIOUSBDeviceClassName);
    if (matchingDict == NULL) {
        fprintf(stderr, "IOServiceMatching returned NULL.\n");
    }

    gNotifyPort = IONotificationPortCreate(kIOMasterPortDefault);
    runLoopSource = IONotificationPortGetRunLoopSource(gNotifyPort);
    gRunLoop = CFRunLoopGetCurrent();
    CFRunLoopAddSource(gRunLoop, runLoopSource, kCFRunLoopDefaultMode);

    kr = IOServiceAddMatchingNotification(gNotifyPort,
                                          kIOFirstMatchNotification,
                                          matchingDict,
                                          DeviceAdded,
                                          NULL,
                                          &gAddedIter);

    DeviceAdded(NULL, gAddedIter);

    CFRunLoopRun(); // 这里是阻塞的,后面的代码永远不会执行!

    // 这些释放代码根本跑不到,注释掉也没用,反而会崩溃
    //CFRelease(matchingDict);
    //CFRelease(gNotifyPort);
    //CFRelease(kr);
    CFRelease(runLoopSource);
    CFRelease(gRunLoop);
}

void DeviceNotification(void *refCon, io_service_t service, natural_t messageType, void *messageArgument) {
    kern_return_t kr;
    DeviceData *deviceDataRef = (DeviceData *) refCon;

    if (messageType == kIOMessageServiceIsTerminated) {
        kr = IOObjectRelease(deviceDataRef->notification);
        
        AppDelegate *appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
        if ((deviceDataRef->serialNum) && (deviceDataRef->deviceName)) {
            [appDelegate updateDeviceDict: (__bridge NSString *)deviceDataRef->serialNum: (__bridge NSString *)deviceDataRef->deviceName: NO];
        }
        
        free(deviceDataRef); // 这里只释放了结构体,里面的CFString没释放!
    }
}

void DeviceAdded(void *refCon, io_iterator_t iterator) {
    kern_return_t kr;
    io_service_t usbDevice;

    while ((usbDevice = IOIteratorNext(iterator))) {
        io_name_t deviceName;
        CFStringRef deviceNameCF;
        DeviceData *deviceDataRef = NULL;

        deviceDataRef = malloc(sizeof(DeviceData));
        bzero(deviceDataRef, sizeof(DeviceData));

        kr = IORegistryEntryGetName(usbDevice, deviceName);
        if (KERN_SUCCESS != kr) {
            deviceName[0] = '\0';
        }

        CFMutableDictionaryRef usbProperties = 0;
        if (IORegistryEntryCreateCFProperties(usbDevice, &usbProperties, kCFAllocatorDefault, kNilOptions) != KERN_SUCCESS) {
            IOObjectRelease(usbDevice);
            continue;
        }
        
        NSDictionary *properties = CFBridgingRelease(usbProperties);
        NSString *serialNum = properties[(__bridge NSString *)CFSTR("USB Serial Number")];
        NSNumber *builtIn = properties[(__bridge NSString *)CFSTR("Built-In")];
        
        deviceNameCF = CFStringCreateWithCString(kCFAllocatorDefault, deviceName, kCFStringEncodingASCII);
        
        if (builtIn.integerValue != 1) {
            deviceDataRef->deviceName = deviceNameCF;
            // 这里的桥接方式有问题,所有权没正确转移
            deviceDataRef->serialNum = (__bridge CFStringRef)(serialNum);
            
            AppDelegate *appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
            // 你的代码在这里截断了,假设后面注册了断开通知
            // kr = IOServiceAddInterestNotification(..., DeviceNotification, deviceDataRef, &deviceDataRef->notification);
        }

        // 这里遗漏了IOObjectRelease(usbDevice)!
    }
}

核心泄漏点分析

我帮你找出了几个关键的泄漏源头:

1. 结构体内部的CFStringRef未释放

你的DeviceData结构体里存储了deviceName和serialNum两个CFStringRef,但在设备断开的DeviceNotification函数里,只free了结构体本身,没有释放这两个CF对象:

  • deviceName是用CFStringCreateWithCString创建的,你拥有它的所有权,必须调用CFRelease释放;
  • serialNum用__bridge桥接自NSString,这不会转移所有权,但如果NSString被ARC释放,你的CFStringRef会变成野指针,同时如果没正确管理引用,也会导致泄漏。

2. IO对象未释放

在DeviceAdded的while循环里,你通过IOIteratorNext(iterator)获取了usbDevice,但很多分支里没有调用IOObjectRelease(usbDevice),IO Kit对象是基于引用计数的,不释放会导致内核对象泄漏。

3. 顶层资源永远无法释放

你在applicationDidFinishLaunching里调用了CFRunLoopRun(),这是一个阻塞调用,后面的所有释放代码(比如CFRelease(gNotifyPort))永远都不会执行,这些全局的通知端口、RunLoop资源会一直占据内存直到进程退出。

4. 桥接方式错误导致的所有权问题

获取serialNum时用了__bridge CFStringRef(serialNum),这只是类型转换,没有保留引用。当properties被ARC释放后,serialNum对应的NSString会被销毁,你的deviceDataRef->serialNum就变成了野指针,同时如果要长期持有这个字符串,必须用__bridge_retained来保留CF引用,后续再释放。

修复方案

针对上面的问题,逐一修复:

修复CFString和结构体的释放

修改DeviceNotification函数,在free(deviceDataRef)前释放内部的CF对象:

void DeviceNotification(void *refCon, io_service_t service, natural_t messageType, void *messageArgument) {
    kern_return_t kr;
    DeviceData *deviceDataRef = (DeviceData *) refCon;

    if (messageType == kIOMessageServiceIsTerminated) {
        kr = IOObjectRelease(deviceDataRef->notification);
        
        AppDelegate *appDelegate = (AppDelegate *)[[NSApplication sharedApplication] delegate];
        if ((deviceDataRef->serialNum) && (deviceDataRef->deviceName)) {
            [appDelegate updateDeviceDict: (__bridge NSString *)deviceDataRef->serialNum: (__bridge NSString *)deviceDataRef->deviceName: NO];
        }
        
        // 释放结构体内部的CF对象
        if (deviceDataRef->deviceName) {
            CFRelease(deviceDataRef->deviceName);
        }
        if (deviceDataRef->serialNum) {
            CFRelease(deviceDataRef->serialNum);
        }
        free(deviceDataRef);
    }
}

修复IO对象的释放

在DeviceAdded的while循环末尾,无论分支如何,都要释放usbDevice:

void DeviceAdded(void *refCon, io_iterator_t iterator) {
    kern_return_t kr;
    io_service_t usbDevice;

    while ((usbDevice = IOIteratorNext(iterator))) {
        // ... 你的原有代码 ...
        
        if (builtIn.integerValue != 1) {
            // ... 处理设备逻辑 ...
        }

        // 必须释放获取到的usbDevice
        IOObjectRelease(usbDevice);
    }
}

调整顶层资源的释放时机

把释放逻辑移到applicationWillTerminate:回调里,因为CFRunLoopRun()会阻塞到RunLoop停止:

- (void)applicationWillTerminate:(NSNotification *)aNotification {
    // 停止RunLoop,让applicationDidFinishLaunching里的阻塞结束
    CFRunLoopStop(gRunLoop);
    
    // 释放IO迭代器
    IOObjectRelease(gAddedIter);
    
    // 释放通知端口
    if (gNotifyPort) {
        CFRelease(gNotifyPort);
        gNotifyPort = NULL;
    }
}

修复桥接方式的所有权问题

获取serialNum时用__bridge_retained保留引用,确保长期持有:

// 替换原来的serialNum赋值
deviceDataRef->serialNum = (__bridge_retained CFStringRef)(serialNum);

这样后续在DeviceNotification里CFRelease(deviceDataRef->serialNum)就能正确释放了。

额外注意事项

  • 确保你在DeviceAdded里正确注册了设备断开的通知(你的代码截断了这部分),比如调用IOServiceAddInterestNotification,并把返回的notification存到deviceDataRef->notification里,这样才能在断开时正确释放。
  • matchingDict的所有权:IOServiceAddMatchingNotification会接管matchingDict的所有权,所以你不需要手动CFRelease它,否则会导致崩溃,这也是你之前注释掉它的原因,这个是对的。

按这些修改后,内存泄漏应该就能解决了!

内容的提问来源于stack exchange,提问作者William Gustafson

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 09:39:50