【问题标题】:Mac Cocoa app GUI freezing while Process runningMac Cocoa 应用程序 GUI 在进程运行时冻结
【发布时间】:2018-06-28 23:34:38
【问题描述】:

我在 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。这使得“取消”按钮或功能性进度条成为不可能。

传输完成后,应用程序会再次运行,并且任何测试输出(例如,打印语句、进度条)都会通过(而不是在备份运行时按预期进行)。

我认为线程可能有助于解决这个问题,所以我查看了这篇关于 Apple 线程的帖子: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

【问题讨论】:

  • 您的程序中是否还有引用task 的代码?启动进程不应自行挂起应用程序。
  • 你在哪里运行任务?您的代码仅显示了您如何创建实例(顺便说一句。通过已弃用的 API;您应该使用 init()),而不是它是如何开始的。
  • @duskwuff 不,没有其他对 task 变量的引用。我曾尝试使用task?.waitUntilExit() 启动,但效果相同。我还尝试使用while task?.isRunning 进行一些测试,但我们对此无能为力。就像程序看不到task正在运行一样。
  • 程序卡住的时候,在调试器中暂停一下,看看主线程的栈。这将向您显示卡在哪里,这通常可以帮助您找出卡住的原因。
  • @AndreasOetjen Process.launchedProcess 部分正在启动进程。我不完全确定它是如何工作的,我在网上找到了它。如何使用init() 启动进程?

标签: swift multithreading cocoa xcode9 freeze


【解决方案1】:

所以,我发现了几件事:

  1. 事实上,launchedProcess 会导致进程立即启动。相反,请使用 init 获得更多控制权
  2. 如果调用waitForExit,那么当前线程会一直等到进程结束。
  3. 当您启动一个进程时,它将独立于您的应用程序运行。因此,如果您退出应用,启动的进程仍会继续运行。

让我们开始吧(在最后完全工作的视图控制器):

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) 并检查 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()
    }
}

【讨论】:

  • 成功了!你真是个天才,太感谢你了!! “取消”按钮实际上仍然不会停止数据传输,但我会弄清楚的。最重要的是应用程序变得无响应,现在已修复。再次感谢!
  • 这解决了我的 UI 被锁定的问题。我的应用程序调用一个 Python 脚本,该脚本本身调用一个 API 并将该进程包装在 DispatchQueue.global().async 中,然后 UI 在 DispatchQueue.main.async 中工作就成功了!
猜你喜欢
  • 1970-01-01
  • 2021-01-22
  • 1970-01-01
  • 2014-10-07
  • 2011-03-09
  • 2011-01-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多