MacOS通过Swift实现监听对应USB的插拔情况

先来看看locationId,在哪里

上述0x14220000 十六进制的数值,即locationID.

Swift 中 监听其物理设备的链接代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import IOKit
import IOKit.usb
import IOKit.usb.IOUSBLib
import Foundation

class USBWatcher {
private var addedIterator: io_iterator_t = 0
private var removedIterator: io_iterator_t = 0
private let targetLocationID: UInt32
private var notificationPort: IONotificationPortRef?
private let eventQueue = DispatchQueue(label: "com.usb.watcher.queue", attributes: .concurrent)

/// target LocationId
/// - Parameter targetLocationID: 目标的Location ID
init(targetLocationID: UInt32) {
self.targetLocationID = targetLocationID

// 在子线程启动监听
eventQueue.async { [weak self] in
self?.setupNotifications()
RunLoop.current.run() // 保持子线程 RunLoop 运行
}
}

deinit {
IOObjectRelease(addedIterator)
IOObjectRelease(removedIterator)
if let port = notificationPort {
IONotificationPortDestroy(port)
}
}

private func setupNotifications() {
let matchDict = IOServiceMatching(kIOUSBDeviceClassName)
// notificationPort = IONotificationPortCreate(kIOMainPortDefault)
if #available(macOS 12.0, *) {
notificationPort = IONotificationPortCreate(kIOMainPortDefault)
} else {
// Fallback on earlier versions
// macOS 10.15 及更早版本使用旧名称
notificationPort = IONotificationPortCreate(kIOMasterPortDefault)
}

guard let port = notificationPort else {
print("Failed to create notification port")
return
}

// 将通知端口绑定到当前线程的 RunLoop(子线程)
let runLoopSource = IONotificationPortGetRunLoopSource(port).takeUnretainedValue()
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .defaultMode)

// 注册设备插入监听
IOServiceAddMatchingNotification(
port,
kIOMatchedNotification,
matchDict,
{ (userData, iterator) in
let watcher = Unmanaged<USBWatcher>.fromOpaque(userData!).takeUnretainedValue()
watcher.handleUSBEvent(iterator: iterator, isConnected: true)
},
Unmanaged.passUnretained(self).toOpaque(),
&addedIterator
)

// 注册设备拔出监听
IOServiceAddMatchingNotification(
port,
kIOTerminatedNotification,
matchDict,
{ (userData, iterator) in
let watcher = Unmanaged<USBWatcher>.fromOpaque(userData!).takeUnretainedValue()
watcher.handleUSBEvent(iterator: iterator, isConnected: false)
},
Unmanaged.passUnretained(self).toOpaque(),
&removedIterator
)

// 初始化枚举设备
handleUSBEvent(iterator: addedIterator, isConnected: true)
handleUSBEvent(iterator: removedIterator, isConnected: false)
}

private func handleUSBEvent(iterator: io_iterator_t, isConnected: Bool) {
while case let device = IOIteratorNext(iterator), device != IO_OBJECT_NULL {
guard let locationID = getDeviceProperty(device: device, key: kUSBDevicePropertyLocationID) as? UInt32,
locationID == targetLocationID else {
IOObjectRelease(device)
continue
}

print("Thread Name == \(Thread.current)")

// 获取设备信息(在子线程)
let vendorID = getDeviceProperty(device: device, key: kUSBVendorID) as? Int ?? 0
let productID = getDeviceProperty(device: device, key: kUSBProductID) as? Int ?? 0
let serialNumber = getDeviceProperty(device: device, key: kUSBSerialNumberString) as? String ?? "N/A"
let deviceName = getDeviceProperty(device: device, key: "USB Product Name") as? String ?? "N/A"


// 切换到主线程发送通知
DispatchQueue.main.async {
let event = isConnected ? true : false
let message = """
[USB 设备] \(event)
├─ Vendor ID:[0x\(String(format: "%04X", vendorID))]
├─ Product ID:[0x\(String(format: "%04X", productID))]
├─ SerialNumber:[\(serialNumber)]
├─ Location ID:[0x\(String(locationID, radix: 16))]
└─ deviceName:[\(deviceName)]
"""
print(message)

// 发送通知(可选)
NotificationCenter.default.post(
name: .USBDeviceStateChanged,
object: nil,
userInfo: [
"event": event,
"vendorID": vendorID,
"productID": productID,
"serialNumber": serialNumber,
"locationID": locationID,
"deviceName": deviceName
]
)
}
IOObjectRelease(device)
}
}

// 通用方法获取设备属性
private func getDeviceProperty(device: io_object_t, key: String) -> Any? {
// print("key == \(key)")
let cfProp = IORegistryEntryCreateCFProperty(
device,
key as CFString,
kCFAllocatorDefault,
0
)
return cfProp?.takeUnretainedValue()
}
}

// 定义通知名称
extension Notification.Name {
static let USBDeviceStateChanged = Notification.Name("USBDeviceStateChanged")
}

使用流程如下:

  1. 先监听
  2. 创建对应locationId 的实体(其内部会有监听触发的逻辑)

调用部分代码如下

  1. 监听部分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
override func viewDidLoad() {
super.viewDidLoad()

// ... code omited ...
// 注册通知监听
NotificationCenter.default.addObserver(
self,
selector: #selector(handleUSBEvent(_:)),
name: .USBDeviceStateChanged,
object: nil
)
}

// 处理 USB 事件
@objc func handleUSBEvent(_ notification: Notification) {
if let userInfo = notification.userInfo,
let event = userInfo["event"] as? Bool,
let vendorID = userInfo["vendorID"] as? Int,
let productID = userInfo["productID"] as? Int,
let serialNumber = userInfo["serialNumber"] as? String,
let deviceName = userInfo["deviceName"] as? String,
let locationID = userInfo["locationID"] as? UInt32 {
let detail_info = """
[\(deviceName)] \(state)
├─ Vendor ID:[0x\(String(format: "%04X", vendorID))]
├─ Product ID:[0x\(String(format: "%04X", productID))]
├─ SerialNumber:[\(serialNumber)]
├─ Location ID:[0x\(String(locationID, radix: 16))]
└─ deviceName:[\(deviceName)]
"""
ICTLog.shared.addLogToFile(msg: detail_info)

}
}

  1. 创建监听实体:
    • 这里需要传入目标监听的locationID,在USBWatcher 类内部会处理监听逻辑
1
2
3
4
5
6
7
8
9
10
11
12
func startWatchUSB() {
// 这里的LocationID 是User 配置的,根据实际情况替换逻辑
guard let locationId = UserDefaults.standard.string(forKey: SaveType.LocationId.rawValue), locationId.count > 3 else { return }

if let number = UInt32(locationId.dropFirst(2), radix: 16) {
// usbWatcher内部会触发通知回调
usbWatcher = USBWatcher(targetLocationID: number)
} else {
print("无效的十六进制字符串")
}

}

综上,在对应LocationID的插拔,就会触发对应的通知,且输出外设的相关信息.

拓展
如何使用命令行获取LocationId?
ioreg -p IOUSB -l -w 0 -x | grep -i "locationID
通过命令行获取所有USB信息
ioreg -p IOUSB -l -w 0 -x (最后一个 -x 是要求16进制)


MacOS通过Swift实现监听对应USB的插拔情况
https://jackiedai.github.io/2025/04/14/015MacOS/003_Swift_LocationId/
Author
lingXiao
Posted on
April 14, 2025
Licensed under