肇鑫的技术博客

业精于勤,荒于嬉

循环结构中异步代码实现灵活退出(下)

循环结构中异步代码实现灵活退出(上)

上一篇中,我们实现了基本结构。但是如果每次都这么做,会比较麻烦。这一篇中,我们尝试将代码封装起来,这样以后我们再做时,只需调用一次就可以了。

首先,我们将之前的代码变成closure。

import Cocoa

class ViewController: NSViewController {
    private let semaphore = DispatchSemaphore(value: 0)
    private let concurrentQueue = DispatchQueue.global()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        concurrentQueue.async {
            self.run()
        }
    }
    
    private func run() {
        for i in 1...10 {
            print(i)
            
            if shouldBreak(i, withClosure: { (result) in
                DispatchQueue.main.async {
                    self.textView.string += "\(i)\n"
                }
                
                if i == 5 {
                    result = true
                }
            }) {
                break
            }
        }
    }
    
    private func shouldBreak(_ i:Int, withClosure closure: @escaping (inout Bool) -> ()) -> Bool {
        var result = false
        let delayInSeconds = Int(arc4random() % 3)
        
        concurrentQueue.asyncAfter(wallDeadline: .now() + .seconds(delayInSeconds)) {
            closure(&result)
            
            self.semaphore.signal()
        }
        
        semaphore.wait()
        
        return result
    }

    @IBOutlet var textView: NSTextView!
}

接下来,我们新建一个文件RangeEnumerator.swift,扩展Sequence

import Foundation

extension Sequence {
    public func breakableForEach(closureWithCondition: @escaping (Element, inout Bool)->()) {
        let semaphore = DispatchSemaphore(value: 0)
        
        DispatchQueue.global().async {
            for e in self {
                if self.shouldBreak(semaphore, e, withClosure: closureWithCondition) {
                    break
                }
            }
        }
    }
    
    private func shouldBreak(_ semaphore:DispatchSemaphore, _ e:Element, withClosure closure: @escaping (Element, inout Bool) -> ()) -> Bool {
        var result = false
        let delayInSeconds = Int(arc4random() % 3)
        
        DispatchQueue.global().asyncAfter(wallDeadline: .now() + .seconds(delayInSeconds)) {
            closure(e, &result)
            semaphore.signal()
        }
        
        semaphore.wait()
        
        return result
    }
}

之后,最初的代码就可以简化为。

class ViewController: NSViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        (1...10).breakableForEach { (i, result) in
            DispatchQueue.main.async {
                self.textView.string += "\(i)\n"
            }

            if i == 5 {
                result = true
            }
        }
    }
    
    @IBOutlet var textView: NSTextView!
}

另外,由于扩展中不能创建存储变量,所以,之前的信号量和队列,就必须放在函数里了。

循环结构中异步代码实现灵活退出(上)

在同步代码时,循环要提前退出十分简单。

for i in 1...10 {
    print(i)
    
    if i == 5 {
        break
    }
}

但是在异步代码中,要灵活退出就不那么容易了。比如,在调用RestAPI时,如果一切正常,就执行下一次循环,如果出错,则进行提示用户,进行重试或者退出。这个就属于异步操作。

import Cocoa

class ViewController: NSViewController {
    private let concurrentQueue = DispatchQueue.global()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        run()
    }
    
    private func run() {
        for i in 1...10 {
            print(i)
            
            if shouldBreak(i) {
                break
            }
        }
    }
    
    private func shouldBreak(_ i:Int) -> Bool {
        var result = false
        let delayInSeconds = Int(arc4random() % 3)

        concurrentQueue.asyncAfter(wallDeadline: .now() + .seconds(delayInSeconds)) {
            if i == 5 {
                result = true
            }
        }
        
        return result
    }
}

由于采用了异步,shouldBreak(_ i:Int) -> Boolresult是先于DispatchQueue.main.async中的代码执行的。因此输出始终是1-10

为保证执行的顺序,需要使用信号量。

import Cocoa

class ViewController: NSViewController {
    private let semaphore = DispatchSemaphore(value: 0)
    private let concurrentQueue = DispatchQueue.global()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        run()
    }
    
    private func run() {
        for i in 1...10 {
            print(i)
            
            if shouldBreak(i) {
                break
            }
        }
    }
    
    private func shouldBreak(_ i:Int) -> Bool {
        var result = false
        let delayInSeconds = Int(arc4random() % 3)

        concurrentQueue.asyncAfter(wallDeadline: .now() + .seconds(delayInSeconds)) {
            if i == 5 {
                result = true
            }
            
            self.semaphore.signal()
        }
        
        semaphore.wait()
        
        return result
    }
}

代码执行正确。下面我们加入改变UI的部分。添加一个NSTextView,让它显示每次的i

import Cocoa

class ViewController: NSViewController {
    private let semaphore = DispatchSemaphore(value: 0)
    private let concurrentQueue = DispatchQueue.global()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        run()
    }
    
    private func run() {
        for i in 1...10 {
            print(i)
            
            if shouldBreak(i) {
                break
            }
        }
    }
    
    private func shouldBreak(_ i:Int) -> Bool {
        var result = false
        let delayInSeconds = Int(arc4random() % 3)

        concurrentQueue.asyncAfter(wallDeadline: .now() + .seconds(delayInSeconds)) {
            DispatchQueue.main.async {
                self.textView.string += "\(i)\n"
            }
            
            if i == 5 {
                result = true
            }
            
            self.semaphore.signal()
        }
        
        semaphore.wait()
        
        return result
    }
    
    @IBOutlet var textView: NSTextView!
}

代码执行后,我们会发现,textView中的i,不是一行一行显示的,而是一开始不显示,然后一下子都显示出来。这和我们期望的不符。

这是什么原因造成的呢?其实,这是因为视图控制器中的代码,默认运行在图形线程,因此semaphore.wait()其实每次都阻塞了图形线程。这导致textView一直没法刷新。直到循环跳出后,界面才成功刷新。

知道了原因,解决办法就有了。将代码从默认的图形线程中移除即可。最终代码:

import Cocoa

class ViewController: NSViewController {
    private let semaphore = DispatchSemaphore(value: 0)
    private let concurrentQueue = DispatchQueue.global()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        concurrentQueue.async {
            self.run()
        }
    }
    
    private func run() {
        for i in 1...10 {
            print(i)
            
            if shouldBreak(i) {
                break
            }
        }
    }
    
    private func shouldBreak(_ i:Int) -> Bool {
        var result = false
        let delayInSeconds = Int(arc4random() % 3)

        concurrentQueue.asyncAfter(wallDeadline: .now() + .seconds(delayInSeconds)) {
            DispatchQueue.main.async {
                self.textView.string += "\(i)\n"
            }
            
            if i == 5 {
                result = true
            }
            
            self.semaphore.signal()
        }
        
        semaphore.wait()
        
        return result
    }
    
    @IBOutlet var textView: NSTextView!
}

循环结构中异步代码实现灵活退出(下)

将协议默认实现作为`selector`进行调用

这个是在swift-users邮件列表遇到的。原标题是Calling default implementation of protocol methods as selectors。原本是个很简单的问题,但是经过几轮讨论,得到的收获还是蛮大的。

先来看问题

有如下代码

protocol Foo: class {
    func bar()
}

extension Foo {
    func bar() {
        print("bar")
    }
}

class Baz: Foo {
    init() {
        let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(bar))  // error: argument of '#selector' refers to instance method 'bar()' that is not exposed to Objective-C
    }
}

编辑器提示无法调用这个#selector,因为它bar()并没有暴露给Objctive-C。而protocol不能插入@objc

如何解决

方法1

不使用协议扩展,而使用基类。

protocol Foo: class {
    func bar()
}

class Base:Foo {
    @objc func bar() {
        print("bar")
    }
}

class Baz: Base {
    override init() {
        super.init()
        let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(bar))
    }
}

方法2

使用一个代理函数。

protocol Foo: class {
    func bar()
}

extension Foo {
    func bar() {
        print("bar")
    }
}

class Baz: Foo {
    init() {
        let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(delegate))
    }
    
    @objc func delegate() {
        bar()
    }
}

这两个办法中,1比2好。因为1方便修改。而如果2需要扩展的话,就需要每个都写一遍。太麻烦。

方法3

我也尝试了convenience init(target: Any, closure: @escaping () -> ())的方法,但是我放弃了。因为我发现,我没法调用closure

后续的邮件中,有人提出了可以内嵌一个类,来实现调用closure。这个是他的实现。

public class BlockTapGestureRecognizer: UITapGestureRecognizer {
    private class Target: NSObject {
        private let closure: (UITapGestureRecognizer) -> ()

        init(closure: @escaping (UITapGestureRecognizer) -> ()) {
            self.closure = closure
            super.init()
        }

        @objc func performClosure(_ sender: Any?) {
            guard let recognizer = sender as? UITapGestureRecognizer else {
                print("Unexpected sender (expected UITapGestureRecognizer)")
                return
            }

            self.closure(recognizer)
        }
    }

    private let target: Target

    public init(closure: @escaping (UITapGestureRecognizer) -> ()) {
        self.target = Target(closure: closure)
        super.init(target: self.target, action: #selector(Target.performClosure(_:)))
    }
}