Mac Cocoa应用程序GUI在进程运行时冻结

时间:2018-06-28 23:34:39

标签: swift multithreading cocoa xcode9 freeze

我已经在Xcode 9中使用Swift创建了一个基本的Mac OS X Cocoa应用程序。该应用程序从用户那里收集源和目标,然后将数据从源传输到目标。通过启动rsync脚本作为进程来完成数据传输:

let path = "/bin/bash"
let arguments = ["/path/to/backup.sh", sourcePath, destinationPath]
task = Process.launchedProcess(launchPath: path, arguments: arguments as! [String])

问题在于,当传输正在运行时,应用程序会旋转彩虹轮,并且无法使用GUI。这样就无法使用“取消”按钮或功能进度条。

传输完成后,应用将再次运行,并且所有测试输出(例如,打印语句,进度条)都会通过(而不是像预期的那样在备份运行时进行)。

我认为线程可能有助于解决此问题,因此我研究了有关苹果线程的帖子:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html

但是,本文中的代码在Xcode中似乎没有正确的语法,因此我不确定如何继续使用线程。

我已经走到了尽头,任何帮助将不胜感激!

运行应用程序后,在调试器中暂停,然后在调试器控制台中键入“ bt”,即为输出:

* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
frame #0: 0x00007fff65fc120a libsystem_kernel.dylib`mach_msg_trap + 10
frame #1: 0x00007fff65fc0724 libsystem_kernel.dylib`mach_msg + 60
frame #2: 0x00007fff3dac4045 CoreFoundation`__CFRunLoopServiceMachPort + 341
frame #3: 0x00007fff3dac3397 CoreFoundation`__CFRunLoopRun + 1783
frame #4: 0x00007fff3dac2a07 CoreFoundation`CFRunLoopRunSpecific + 487
frame #5: 0x00007fff3cda0d96 HIToolbox`RunCurrentEventLoopInMode + 286
frame #6: 0x00007fff3cda0b06 HIToolbox`ReceiveNextEventCommon + 613
frame #7: 0x00007fff3cda0884 HIToolbox`_BlockUntilNextEventMatchingListInModeWithFilter + 64
frame #8: 0x00007fff3b053a73 AppKit`_DPSNextEvent + 2085
frame #9: 0x00007fff3b7e9e34 AppKit`-[NSApplication(NSEvent) _nextEventMatchingEventMask:untilDate:inMode:dequeue:] + 3044
frame #10: 0x00007fff3b048885 AppKit`-[NSApplication run] + 764
frame #11: 0x00007fff3b017a72 AppKit`NSApplicationMain + 804
* frame #12: 0x000000010000a88d Mac Syncy`main at AppDelegate.swift:12
frame #13: 0x00007fff65e7a015 libdyld.dylib`start + 1
frame #14: 0x00007fff65e7a015 libdyld.dylib`start + 1

1 个答案:

答案 0 :(得分:2)

所以,我发现了几件事:

  1. 实际上,launchedProcess使该过程立即启动。而是使用init来获得更多控制权
  2. 如果您调用waitForExit,则当前线程将一直等到进程结束。
  3. 启动流程时,它将独立于您的应用运行。因此,如果您退出应用程序,则启动的过程仍将继续运行。

因此,让我们开始(最后完全使用View Controller):

1。创建流程

task = Process.init()
task.launchPath = path
task.arguments = arguments
task.currentDirectoryPath = workDir
// not running yet, so let's start it:
task.lauch()

2。异步等待子进程结束

DispatchQueue.global().async {
    print ("wating for exit")
    self.task.waitUntilExit()
}

3。杀死子进程

您必须终止任务,例如在应用程序委托的applicationWillTerminate中。

不过,您应该意识到,这可能会使您的(rsync)操作保持未定状态-文件/目录仅被半复制等。

4。奖金:进度指示器

我认为提供进度指示器的唯一方法是解析Process(task.standardOutput)的输出并检查Wheatr rsync在这里提供有用的信息。但这是一个全新的故事,抱歉,这里没有代码。

代码

这是一个具有开始和取消按钮的视图控制器。 请记住,对于已发布的应用程序,您将必须提供更多的错误检查。

class ViewController: NSViewController {

    var task:Process!
    var out:FileHandle?
    var outputTimer: Timer?


    @IBAction func startPressed(_ sender: NSButton) {
        print("** starting **")

        let path = "/bin/bash"
        let workDir = "/path/to/working/folder"
        let sourcePath = "source"
        let destinationPath = "destination"

        let arguments = ["backup.sh", sourcePath, destinationPath]
        task = Process.init()
        task.launchPath = path
        task.arguments = arguments
        task.currentDirectoryPath = workDir

        self.task.launch()
        DispatchQueue.global().async {
            // this runs in a worker thread, so the UI remains responsive
            print ("wating for exit")
            self.task.waitUntilExit()
            DispatchQueue.main.async {
                // do so in the main thread
                if let timer = self.outputTimer {
                    timer.invalidate()
                    self.outputTimer = nil
                }
            }
        }

    }


    @IBAction func cancelPressed(_ sender: NSButton) {
        print("** cancelling **")
        if let timer = self.outputTimer {
            timer.invalidate()
            self.outputTimer = nil
        }
        task.interrupt()
    }
}