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

将NSFilePromiseProvider写入粘贴板导致Mac应用退出卡顿

问题解析:NSFilePromiseProvider写入粘贴板导致应用退出延迟

问题背景

在Mac应用中执行「编辑→复制」操作时,会将以kUTTypeFileURL初始化的NSFilePromiseProvider对象写入通用粘贴板,以此实现文件向Finder等应用复制。但退出应用时会出现严重延迟(含沙滩球卡顿),控制台日志显示存在粘贴板活动,系统似乎在确保文件承诺仍可被提供。

目前找到的所有示例均是在表格视图拖放场景中使用NSFilePromiseProvider,未发现直接将其写入粘贴板的案例(尽管它实现了NSPasteboardWriting协议)。

经测试发现:若自定义的NSFilePromiseProvider在writingOptions(forType:pasteboard:)方法中对所有类型返回[],应用即可无延迟关闭。现需解析:

  1. 问题最初产生的原因
  2. 此规避方案生效的原理

相关代码实现

自定义FilePromiseProvider

class FilePromiseProvider: NSFilePromiseProvider {

    struct UserInfoKeys {
        static let fileURL = "fileURLKey"
        static let row = "rowKey"
    }

    override func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
        var types = super.writableTypes(for: pasteboard)
        if let userInfoDict = userInfo as? [String: Any] {
            if userInfoDict[UserInfoKeys.row] != nil {
                types.append(.fileListTableRow)
            }
            if userInfoDict[UserInfoKeys.fileURL] != nil {
                types.append(.fileURL)
                types.append(.string)
            }
        }
        return types
    }

    override func writingOptions(forType type: NSPasteboard.PasteboardType, pasteboard: NSPasteboard) -> NSPasteboard.WritingOptions {
        // 返回[]时应用无延迟关闭,默认实现会返回.promise导致系统额外处理
        return []
    }

    override func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
        guard let userInfoDict = userInfo as? [String: Any] else { return nil }

        switch type {
        case .fileListTableRow:
            if let row = userInfoDict[UserInfoKeys.row] as? Int {
                return row
            }

        case .fileURL:
            if let url = userInfoDict[UserInfoKeys.fileURL] as? NSURL {
                return url.pasteboardPropertyList(forType: type)
            }

        case .string:
            if let url = userInfoDict[UserInfoKeys.fileURL] as? NSURL {
                return url.lastPathComponent
            }

        default: break
        }

        return super.pasteboardPropertyList(forType: type)
    }
}

复制操作写入粘贴板代码

@IBAction func copy(_ sender: Any?) {
    guard let urls = representedObject as? [URL] else { return }

    let itemIndexes = tableView.selectedRowIndexes
    if itemIndexes.isEmpty {
        return
    }

    var filePromises = [FilePromiseProvider]()
    for idx in itemIndexes {
        var userInfo = [String: Any]()
        userInfo[FilePromiseProvider.UserInfoKeys.fileURL] = urls[idx]

        let filePromise = FilePromiseProvider(fileType: UTType.fileURL.identifier, delegate: self)
        filePromise.userInfo = userInfo
        filePromises.append(filePromise)
    }

    NSPasteboard.general.clearContents()
    NSPasteboard.general.writeObjects(filePromises)
}

NSFilePromiseProviderDelegate实现

extension ViewController: NSFilePromiseProviderDelegate {

    func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, fileNameForType fileType: String) -> String {
        let fileURL = queryFileURL(from: filePromiseProvider)
        return fileURL?.lastPathComponent ?? ""
    }

    func filePromiseProvider(_ filePromiseProvider: NSFilePromiseProvider, writePromiseTo url: URL, completionHandler: @escaping (Error?) -> Void) {

        do {
            if let atURL = queryFileURL(from: filePromiseProvider) {
                try FileManager.default.copyItem(at: atURL, to: url)
            }
            completionHandler(nil)
        } catch let error {
            OperationQueue.main.addOperation {
                self.presentError(error, modalFor: self.view.window!, delegate: nil, didPresent: nil, contextInfo: nil)
            }
            completionHandler(error)
        }
    }

    func operationQueue(for filePromiseProvider: NSFilePromiseProvider) -> OperationQueue {
        return filePromiseQueue
    }

    private func queryFileURL(from filePromiseProvider: NSFilePromiseProvider) -> URL? {
        if let userInfo = filePromiseProvider.userInfo as? [String: Any] {
           return userInfo[FilePromiseProvider.UserInfoKeys.fileURL] as? URL
        }
        return nil
    }
}

技术解析

1. 问题产生的原因

NSFilePromiseProvider的默认writingOptions(forType:pasteboard:)实现会对文件承诺相关类型返回.promise选项。当你将该对象写入粘贴板后:

  • 系统会认定这些文件承诺需要由你的应用持续提供,直到接收方(如Finder)完成文件接收。
  • 当应用退出时,系统会检查粘贴板上的活跃承诺:若存在标记为.promise的项,系统会触发内部流程,要么等待应用完成承诺,要么将文件预先拷贝到系统临时目录,这个过程会阻塞应用退出,导致延迟和卡顿。
  • 你在writableTypes中额外添加了.fileURL和.string类型,默认实现会对这些类型也应用.promise逻辑,进一步增加了系统的处理开销。

2. 规避方案生效的原理

当你在writingOptions(forType:pasteboard:)中返回空数组[]时:

  • 相当于告诉系统,所有粘贴板类型的内容都是即时提供的,而非承诺式的。系统无需再跟踪这些粘贴板项的提供者状态。
  • 应用退出时,系统不会触发额外的承诺验证或文件备份流程,因此不会出现阻塞延迟。
  • 你的pasteboardPropertyList(forType:)方法已经为.fileURL和.string类型提供了直接的属性列表值,完全不需要依赖承诺机制,所以返回空选项不会影响文件复制功能的正常运行。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 12:35:08