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

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:申请辅助功能权限(简单但需要用户授权)

如果你的应用可以接受让用户手动授权,那这个方案最直接:

  1. 先在Info.plist中添加权限说明,告诉用户为什么需要这个权限:
<key>NSAccessibilityUsageDescription</key>
<string>需要访问辅助功能来检测USB设备的挂载与卸载</string>
  1. 在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 07:27:40