Swift 3 线程中的 CFRunLoopRun?

Swift 3 CFRunLoopRun in Thread?

我刚刚制作了一个简单的测试应用程序来显示击键的键码和修饰符。它适用于 3 次击键,然后应用程序崩溃。当它崩溃时,调试控制台最后只显示 (LLDB) 。任何建议可能是什么原因造成的?也许与线程或指针有关,但我不确定如何解决这个问题。我包括下面的代码。我真的很感激任何帮助!谢谢!

import Cocoa
import Foundation

class ViewController: NSViewController {

    @IBOutlet weak var textField: NSTextFieldCell!
    let speech:NSSpeechSynthesizer = NSSpeechSynthesizer()

    func update(msg:String) {
        textField.stringValue = msg
        print(msg)
        speech.startSpeaking(msg)
    }

    func bridgeRetained<T : AnyObject>(obj : T) -> UnsafeRawPointer {
        return UnsafeRawPointer(Unmanaged.passRetained(obj).toOpaque())
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        DispatchQueue.global().async {
            func myCGEventCallback(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {

                let parent:ViewController = Unmanaged<ViewController>.fromOpaque(refcon!).takeRetainedValue()

                if [.keyDown].contains(type) {
                    let flags:CGEventFlags =     event.flags
                    let pressed = Modifiers(rawValue:flags.rawValue)
                    var msg = ""

                    if pressed.contains(Modifiers(rawValue:CGEventFlags.maskAlphaShift.rawValue)) {
                        msg+="caps+"
                    }
                    if pressed.contains(Modifiers(rawValue:CGEventFlags.maskShift.rawValue)) {
                        msg+="shift+"
                    }
                    if pressed.contains(Modifiers(rawValue:CGEventFlags.maskControl.rawValue)) {
                        msg+="control+"
                    }
                    if pressed.contains(Modifiers(rawValue:CGEventFlags.maskAlternate.rawValue)) {
                        msg+="option+"
                    }
                    if pressed.contains(Modifiers(rawValue:CGEventFlags.maskCommand.rawValue)) {
                        msg += "command+"
                    }
                    if pressed.contains(Modifiers(rawValue:CGEventFlags.maskSecondaryFn.rawValue)) {
                        msg += "function+"
                    }

                    var keyCode = event.getIntegerValueField(.keyboardEventKeycode)
                    msg+="\(keyCode)"

                    DispatchQueue.main.async {
                        parent.update(msg:msg)
                    }

                    if keyCode == 0 {
                        keyCode = 6
                    } else if keyCode == 6 {
                        keyCode = 0
                    }

                    event.setIntegerValueField(.keyboardEventKeycode, value: keyCode)
                }
                return Unmanaged.passRetained(event)
            }

            let eventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue)

            guard let eventTap = CGEvent.tapCreate(tap: .cgSessionEventTap, place: .headInsertEventTap, options: .defaultTap, eventsOfInterest: CGEventMask(eventMask), callback:  myCGEventCallback, userInfo: UnsafeMutableRawPointer(mutating: self.bridgeRetained(obj: self))) else {
                print("failed to create event tap")
                exit(1)
            }
            let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
            CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
            CGEvent.tapEnable(tap: eventTap, enable: true)
            CFRunLoopRun()
        }
        // Do any additional setup after loading the view.
    }

    override var representedObject: Any? {
        didSet {
            // Update the view, if already loaded.
        }
    }

}

主要问题是引用计数:您创建了一个 retained 在安装事件处理程序时引用视图控制器,这只发生一次。 然后你 consume 回调中的引用,这发生在每个 点击事件。因此,引用计数最终会降为零,并且 视图控制器被释放,导致崩溃。

最好将未保留的引用传递给回调,并注意 当视图控制器被释放时,事件处理程序被卸载。

此外,无需为 OS X 应用程序创建单独的 运行 循环,或异步调度处理程序创建。

使回调成为全局函数,而不是方法。采用 takeUnretainedValue() 获取视图控制器引用:

func myCGEventCallback(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {

    let viewController = Unmanaged<ViewController>.fromOpaque(refcon!).takeUnretainedValue()
    if type == .keyDown {

        var keyCode = event.getIntegerValueField(.keyboardEventKeycode)
        let msg = "\(keyCode)"

        DispatchQueue.main.async {
            viewController.update(msg:msg)
        }

        if keyCode == 0 {
            keyCode = 6
        } else if keyCode == 6 {
            keyCode = 0
        }
        event.setIntegerValueField(.keyboardEventKeycode, value: keyCode)
    }
    return Unmanaged.passRetained(event)
}

在视图控制器中,保留对 运行 循环源的引用 这样您就可以在 deinit 中删除它,然后使用 passUnretained() 将指向视图控制器的指针传递给 回调:

class ViewController: NSViewController {

    var eventSource: CFRunLoopSource?

    override func viewDidLoad() {
        super.viewDidLoad()

        let eventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue)
        let userInfo = UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())

        if let eventTap = CGEvent.tapCreate(tap: .cgSessionEventTap, place: .headInsertEventTap,
                                         options: .defaultTap, eventsOfInterest: CGEventMask(eventMask),
                                         callback: myCGEventCallback, userInfo: userInfo) {
            self.eventSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
            CFRunLoopAddSource(CFRunLoopGetCurrent(), self.eventSource, .commonModes)
        } else {
            print("Could not create event tap")
        }
    }

    deinit {
        if let eventSource = self.eventSource {
            CFRunLoopRemoveSource(CFRunLoopGetCurrent(), eventSource, .commonModes)
        }
    }

    // ...

}

另一种选择是 install/uninstall 中的事件处理程序 viewDidAppearviewDidDisappear.