基于USBPrivateDataSample适配的IOKit代码内存泄漏排查
嘿,我来帮你揪出这个内存泄漏的问题!先理清楚你的情况:你需要监听所有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

