【问题标题】:How to monitor a folder for new files in swift?如何快速监控文件夹中的新文件?
【发布时间】:2014-06-10 20:13:50
【问题描述】:

如何在没有轮询的情况下快速监控文件夹中的新文件(效率非常低)?我听说过诸如 kqueue 和 FSEvents 之类的 API,但我不确定是否可以快速实现它们?

【问题讨论】:

标签: macos swift


【解决方案1】:

GCD 似乎是要走的路。 NSFilePresenter 类不能正常工作。它们有问题,坏了,Apple 在过去的 4 年里一直不愿意修复它们。可能会被弃用。

这是一篇很好的帖子,描述了这项技术的要点。

"Handling Filesystem Events with GCD"大卫·哈姆里克

从网站引用的示例代码。我将他的 C 代码翻译成 Swift。

    let fildes = open("/path/to/config.plist", O_RDONLY)

    let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
    let source = dispatch_source_create(
        DISPATCH_SOURCE_TYPE_VNODE,
        UInt(fildes),
        DISPATCH_VNODE_DELETE | DISPATCH_VNODE_WRITE | DISPATCH_VNODE_EXTEND | DISPATCH_VNODE_ATTRIB | DISPATCH_VNODE_LINK | DISPATCH_VNODE_RENAME | DISPATCH_VNODE_REVOKE,
        queue)

    dispatch_source_set_event_handler(source,
        {
            //Reload the config file
        })

    dispatch_source_set_cancel_handler(source,
        {
            //Handle the cancel
        })

    dispatch_resume(source);

    ...

        // sometime later
        dispatch_source_cancel(source);

作为参考,这是作者发布的另一个QA:


如果您有兴趣观看目录,这里有另一篇描述它的帖子。

"Monitoring a Folder with GCD"Cocoanetics 上。 (不幸的是,我找不到作者的名字。我很抱歉缺少署名)

唯一明显的区别是获取文件描述符。这使得目录的事件通知仅文件描述符。

_fileDescriptor = open(path.fileSystemRepresentation(), O_EVTONLY)

更新

之前我声称FSEvents API 不工作,但我错了。该 API 运行良好,如果您有兴趣观看深度文件树,那么它的简单性可能比 GCD 更好。

无论如何,FSEvents 不能在纯 Swift 程序中使用。因为它需要传递 C 回调函数,而 Swift 目前不支持它(Xcode 6.1.1)。然后我不得不退回到 Objective-C 并再次包装它。

此外,任何此类 API 都是完全异步的。这意味着在您收到通知时,实际的文件系统状态可能会有所不同。那么精确或准确的通知并没有真正的帮助,仅对标记脏标志有用。

更新 2

我最终为 Swift 编写了一个围绕 FSEvents 的包装器。 这是我的工作,希望对您有所帮助。

【讨论】:

  • 不错。我试图“听”一次被写入的文件夹,但因为无法让FSEvents 正常工作而放弃了。我一直在等待由相机驱动程序写入非常大的图像文件,并且在文件“准备好”打开、以黑色图像结束等的正确时刻得到通知。另外,我看到了一些广播的消息不一致,很难解释它们。也许我没有正确理解文档,也没有遇到好的示例代码。肯定会尝试 GCD。
  • 谢谢。我刚刚通过直接从 GitHub 克隆来使用您的框架,并关注您的视频并构建!第一次工作。所以,现在我只需要学习一些 Swift 来使用它! :-)
  • 嘿@Eonil 你知道我如何检测文件何时更新/修改吗?我看到了您的近似值,并且我尝试了更多我发现的方法,但是当文件更新时没有触发。
  • NSFilePresenter 不是一个类,它是一个协议。它本身没有任何功能。它通常与NSFileCoordinator 一起使用,并且此类不是用于监视目录,而是用于在单个进程内以及跨进程边界协调文件系统上的读/写 I/O 操作,并且使用 NSFileCoordinator 可以完美地工作。跨度>
【解决方案2】:

我改编了 Stanislav Smida 的代码,使其与 Xcode 8 和 Swift 3 一起使用

class DirectoryObserver {

    private let fileDescriptor: CInt
    private let source: DispatchSourceProtocol

    deinit {

      self.source.cancel()
      close(fileDescriptor)
    }

    init(URL: URL, block: @escaping ()->Void) {

      self.fileDescriptor = open(URL.path, O_EVTONLY)
      self.source = DispatchSource.makeFileSystemObjectSource(fileDescriptor: self.fileDescriptor, eventMask: .all, queue: DispatchQueue.global())
      self.source.setEventHandler { 
          block()
      }
      self.source.resume()
  }

}

【讨论】:

  • 用 Swift 4.2 测试过,仍然很好用。谢谢!
  • 2021 年。代码有效。很棒的文件监视器!看起来只有一个没有子目录的目录。只是我搜索的想法!非常感谢!
【解决方案3】:

最简单的解决方案是使用 Apple 的 DirectoryMonitor.swift https://developer.apple.com/library/mac/samplecode/Lister/Listings/ListerKit_DirectoryMonitor_swift.html

var dm = DirectoryMonitor(URL: AppDelegate.applicationDocumentsDirectory)
dm.delegate = self
dm.startMonitoring()

【讨论】:

【解决方案4】:

SKQueue 是一个围绕 kqueue 的 Swift 包装器。这是监视目录并通知写入事件的示例代码。

class SomeClass: SKQueueDelegate {
  func receivedNotification(_ notification: SKQueueNotification, path: String, queue: SKQueue) {
    print("\(notification.toStrings().map { $0.rawValue }) @ \(path)")
  }
}

if let queue = SKQueue() {
  let delegate = SomeClass()

  queue.delegate = delegate
  queue.addPath("/some/file/or/directory")
  queue.addPath("/some/other/file/or/directory")
}

【讨论】:

    【解决方案5】:

    用于目录监视器的 Swift 5 版本,带有 GCD,来自Apple

    import Foundation
    
    /// A protocol that allows delegates of `DirectoryMonitor` to respond to changes in a directory.
    protocol DirectoryMonitorDelegate: class {
        func directoryMonitorDidObserveChange(directoryMonitor: DirectoryMonitor)
    }
    
    class DirectoryMonitor {
        // MARK: Properties
    
        /// The `DirectoryMonitor`'s delegate who is responsible for responding to `DirectoryMonitor` updates.
        weak var delegate: DirectoryMonitorDelegate?
    
        /// A file descriptor for the monitored directory.
        var monitoredDirectoryFileDescriptor: CInt = -1
    
        /// A dispatch queue used for sending file changes in the directory.
        let directoryMonitorQueue =  DispatchQueue(label: "directorymonitor", attributes: .concurrent)
    
        /// A dispatch source to monitor a file descriptor created from the directory.
        var directoryMonitorSource: DispatchSource?
    
        /// URL for the directory being monitored.
        var url: URL
    
        // MARK: Initializers
        init(url: URL) {
            self.url = url
        }
    
        // MARK: Monitoring
    
        func startMonitoring() {
            // Listen for changes to the directory (if we are not already).
            if directoryMonitorSource == nil && monitoredDirectoryFileDescriptor == -1 {
                // Open the directory referenced by URL for monitoring only.
                monitoredDirectoryFileDescriptor = open((url as NSURL).fileSystemRepresentation, O_EVTONLY)
    
                // Define a dispatch source monitoring the directory for additions, deletions, and renamings.
                directoryMonitorSource = DispatchSource.makeFileSystemObjectSource(fileDescriptor: monitoredDirectoryFileDescriptor, eventMask: DispatchSource.FileSystemEvent.write, queue: directoryMonitorQueue) as? DispatchSource
    
                // Define the block to call when a file change is detected.
                directoryMonitorSource?.setEventHandler{
                    // Call out to the `DirectoryMonitorDelegate` so that it can react appropriately to the change.
                    self.delegate?.directoryMonitorDidObserveChange(directoryMonitor: self)
                }
    
                // Define a cancel handler to ensure the directory is closed when the source is cancelled.
                directoryMonitorSource?.setCancelHandler{
                    close(self.monitoredDirectoryFileDescriptor)
    
                    self.monitoredDirectoryFileDescriptor = -1
    
                    self.directoryMonitorSource = nil
                }
    
                // Start monitoring the directory via the source.
                directoryMonitorSource?.resume()
            }
        }
    
        func stopMonitoring() {
            // Stop listening for changes to the directory, if the source has been created.
            if directoryMonitorSource != nil {
                // Stop monitoring the directory via the source.
                directoryMonitorSource?.cancel()
            }
        }
    }
    
    

    【讨论】:

      【解决方案6】:

      我尝试过使用这几行代码。到目前为止似乎有效。

      class DirectoryObserver {
      
          deinit {
      
              dispatch_source_cancel(source)
              close(fileDescriptor)
          }
      
          init(URL: NSURL, block: dispatch_block_t) {
      
              fileDescriptor = open(URL.path!, O_EVTONLY)
              source = dispatch_source_create(DISPATCH_SOURCE_TYPE_VNODE, UInt(fileDescriptor), DISPATCH_VNODE_WRITE, dispatch_queue_create(nil, DISPATCH_QUEUE_CONCURRENT))
              dispatch_source_set_event_handler(source, { dispatch_async(dispatch_get_main_queue(), block) })
              dispatch_resume(source)
          }
      
          //
      
          private let fileDescriptor: CInt
          private let source: dispatch_source_t
      }
      

      确保不要进入保留周期。如果您要在块中使用此实例的所有者,请安全地进行。例如:

      self.directoryObserver = DirectoryObserver(URL: URL, block: { [weak self] in
      
          self?.doSomething()
      })
      

      【讨论】:

      • 很棒的代码。下面是一个监控桌​​面的例子: let theURL = NSURL("~/Desktop/".stringByExpandingTildeInPath)
      • 您的代码仅在添加或删除文件时有效。不是文件更改。无论如何都很棒。我需要监控文件更改,所以我一直在寻找。
      • FSEvent 似乎是我正在寻找的。​​span>
      【解决方案7】:

      我发现我目前正在使用的最简单的方法是这个很棒的库:https://github.com/eonist/FileWatcher

      From README

      安装:

      • CocoaPods pod "FileWatcher"
      • 迦太基github "eonist/FileWatcher" "master"
      • 手动开启FileWatcherExample.xcodeproj
      let filewatcher = FileWatcher([NSString(string: "~/Desktop").expandingTildeInPath])
      
      filewatcher.callback = { event in
        print("Something happened here: " + event.path)
      }
      
      filewatcher.start() // start monitoring
      

      【讨论】:

      • 但是效率高吗?
      • 非常适合我。没有性能问题。你的用例是什么?
      【解决方案8】:

      我遇到了任何答案中都没有提到的问题。由于我的应用程序使用 UIDocumentBrowserViewController(即 Apple 自己的 Files 应用程序)来管理其文档,因此我无法控制用户的习惯。我使用SKQueue 监控所有文件以保持元数据同步,但在某个时候应用程序开始崩溃。

      事实证明,一个应用程序可以同时打开的文件描述符上限为 256 个,即使只是为了监控也是如此。我最终将 SKQueue 和 Apple 的 Directory Monitor(您可以在当前线程的 this answer 中找到的参考)结合起来创建了一个名为 SFSMonitor 的类,它使用 Dispatch Sources 监视整个文件或目录队列。

      我在this SO thread 中详细介绍了我的发现和我现在使用的做法。

      【讨论】:

      • 它是否监控子目录?
      • 每个目录和子目录都作为一个单独的实体进行监控。假设您将一个子目录复制到一个受监控的目录中:您将收到该更改的通知,但您不会知道该子目录中发生的任何事情。您必须检查添加了什么,当您意识到它是一个子目录时,将其添加到您的监视列表中。换句话说 - 观察不是递归的,就原始目录而言,子目录只是另一个条目。
      【解决方案9】:

      您可以将 UKKQueue 添加到您的项目中。请参阅http://zathras.de/angelweb/sourcecode.htm,它很容易使用。 UKKQueue 是用 Objective C 编写的,但你可以从 swift 中使用它

      【讨论】:

        【解决方案10】:

        根据您的应用需求,您或许可以使用简单的解决方案。

        我实际上在生产产品中使用了 kqueue;我对性能并不疯狂,但它确实有效,所以我并没有想太多,直到我找到了一个可以更好地满足我的需求的小技巧,此外,它使用的资源更少,这对于性能密集型来说很重要程式。

        再次,如果您的项目允许,您可以做的是,每次切换到应用程序时,您只需检查文件夹作为逻辑的一部分,而不必使用 kqueue 定期检查文件夹。这很有效,而且使用的资源要少得多。

        【讨论】:

        • 我认为 Atom.io 在断言文件是否已通过 git 推送时会这样做。必须在应用程序之间来回切换才能看到变化,这非常烦人。这对我来说是一种 UI/UX 反模式。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-11-28
        • 2021-08-08
        • 1970-01-01
        • 2021-04-09
        • 2011-01-10
        • 2013-10-24
        相关资源
        最近更新 更多