NSWorkspace挂载/卸载通知未触发,求排查macOS后台应用问题
看起来你的问题出在两个关键地方:UsbDetector实例被提前回收,以及纯后台应用无法接收NSWorkspace通知的系统限制,咱们一步步来解决:
1. 先解决实例被释放的问题
你在applicationDidFinishLaunching里用_ = UsbDetector()创建了实例,但这个对象没有被任何强引用持有——方法执行完后,ARC(自动引用计数)就会把它回收掉,自然收不到后续的挂载/卸载通知。
修改AppDelegate,把UsbDetector作为类属性保存下来,确保它能一直存活:
import Cocoa import SwiftUI @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { var window: NSWindow! // 添加这个属性来持久持有UsbDetector实例 private var usbDetector: UsbDetector? func applicationDidFinishLaunching(_ aNotification: Notification) { print("Hello from AppDelegate") // 初始化并持有实例 usbDetector = UsbDetector() // ... 其余窗口创建代码不变 } // ... 其余代码不变 }
2. 解决后台应用无法接收NSWorkspace通知的问题
当你设置LSBackgroundOnly为true后,你的应用属于纯后台应用,系统默认不会把NSWorkspace的didMountNotification和didUnmountNotification发送给这类应用——因为系统认为后台应用不需要监听桌面级的挂载事件。这里有两个可行的解决方案:
方案A:申请辅助功能权限(简单但需要用户授权)
如果你的应用可以接受让用户手动授权,那这个方案最直接:
- 先在Info.plist中添加权限说明,告诉用户为什么需要这个权限:
<key>NSAccessibilityUsageDescription</key> <string>需要访问辅助功能来检测USB设备的挂载与卸载</string>
- 在UsbDetector初始化时请求授权(或者在App启动的时机):
import Foundation import AppKit import ApplicationServices class UsbDetector { init() { print("Hello from UsbDetector") // 请求辅助功能授权 let options: NSDictionary = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as NSString: true] let isTrusted = AXIsProcessTrustedWithOptions(options) if !isTrusted { print("请在系统偏好设置 → 安全性与隐私 → 隐私 → 辅助功能中启用本应用") } // 注册通知的代码不变 let notificationCenter = NSWorkspace.shared.notificationCenter notificationCenter.addObserver(self, selector: #selector(didMount(_:)), name: NSWorkspace.didMountNotification, object: nil) notificationCenter.addObserver(self, selector: #selector(didUnmount(_:)), name: NSWorkspace.didUnmountNotification, object: nil) } // ... 其余didMount、didUnmount方法不变 }
用户授权后,后台应用就能正常接收NSWorkspace的挂载/卸载通知了。
方案B:使用IOKit直接监听USB设备(无需用户授权,更底层)
如果不想让用户手动授权,推荐用IOKit来监听USB设备的底层连接/断开事件——这是系统级的监听方式,不受后台应用的限制,更适合静默运行的工具。
下面是一个适配你需求的简化版IOKit实现:
import Foundation import IOKit import IOKit.usb class UsbDetector { private var notificationPort: IONotificationPortRef? private var iterator: io_iterator_t = 0 init() { print("Hello from UsbDetector") setupUsbMonitoring() } deinit { // 清理监听资源 if iterator != 0 { IOObjectRelease(iterator) } if let port = notificationPort { IONotificationPortDestroy(port) } } private func setupUsbMonitoring() { // 创建USB设备的匹配字典 let matchingDict = IOServiceMatching(kIOUSBDeviceClassName) as NSMutableDictionary // 如果你需要过滤特定设备,可以添加厂商ID/产品ID的条件 // matchingDict[kUSBVendorID] = NSNumber(value: 0x1234) // 创建通知端口 let notificationPort = IONotificationPortCreate(kIOMainPortDefault) self.notificationPort = notificationPort // 设备连接的回调 let connectCallback: IOServiceMatchingCallback = { (iterator, userData) in let detector = Unmanaged<UsbDetector>.fromOpaque(userData!).takeUnretainedValue() detector.handleUsbConnected(iterator: iterator) } // 设备断开的回调 let disconnectCallback: IOServiceMatchingCallback = { (iterator, userData) in let detector = Unmanaged<UsbDetector>.fromOpaque(userData!).takeUnretainedValue() detector.handleUsbDisconnected(iterator: iterator) } // 注册设备连接通知 IOServiceAddMatchingNotification( notificationPort, kIOMatchedNotification, matchingDict, connectCallback, Unmanaged.passUnretained(self).toOpaque(), &iterator ) // 处理当前已连接的设备 handleUsbConnected(iterator: iterator) // 注册设备断开通知 IOServiceAddMatchingNotification( notificationPort, kIOTerminatedNotification, matchingDict, disconnectCallback, Unmanaged.passUnretained(self).toOpaque(), &iterator ) // 处理当前已断开的设备(可选) handleUsbDisconnected(iterator: iterator) // 将通知端口加入RunLoop,确保能收到事件 CFRunLoopAddSource( CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(notificationPort).takeUnretainedValue(), CFRunLoopMode.defaultMode ) } private func handleUsbConnected(iterator: io_iterator_t) { var device: io_object_t while case let device = IOIteratorNext(iterator), device != 0 { defer { IOObjectRelease(device) } // 获取设备名称(可以根据需求获取更多属性,比如路径、厂商ID等) if let deviceName = getDeviceProperty(device: device, key: kUSBProductString) as? String { print("USB设备已连接:\(deviceName)") } } } private func handleUsbDisconnected(iterator: io_iterator_t) { var device: io_object_t while case let device = IOIteratorNext(iterator), device != 0 { defer { IOObjectRelease(device) } if let deviceName = getDeviceProperty(device: device, key: kUSBProductString) as? String { print("USB设备已断开:\(deviceName)") } } } // 通用方法:获取设备的属性值 private func getDeviceProperty(device: io_object_t, key: CFString) -> Any? { let property = IORegistryEntryCreateCFProperty(device, key, kCFAllocatorDefault, 0) defer { property?.release() } return property?.takeUnretainedValue() } }
这个实现不需要用户授权,直接监听系统的USB设备事件,在Catalina 10.15.5和Xcode 11.5环境下完全兼容。
总结
先解决实例被释放的问题,这是基础;然后根据你的需求选择方案:如果可以接受用户授权,选方案A;如果需要完全静默运行,方案B是更可靠的选择。
内容的提问来源于stack exchange,提问作者André Gasser

