肇鑫的技术博客

业精于勤,荒于嬉

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

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

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!
}

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

AVMovie始终显示视频时长为0问题的分析、解决

推特仅支持0.5秒到140秒之内长度的视频,因此上传前要先检验视频的长度。测试时,我发现,Finder分享的视频时长始终为0。

我查询了一下,最初看到的说法是,AVAssetAVMovie的父类)的加载是异步的,不能够立即获得时长,而需要用键值观察的方法,注册通知来观察duration这个属性何时变化。昨晚看到这里,我就睡了。

今天再看,发现有协议AVAsynchronousKeyValueLoading,可以用来异步处理键值。方法loadValuesAsynchronously(forKeys:completionHandler:)会在加载完成后执行completionHandler。这样就不用我们自己手动注册键值观察的通知了。

但是,我测试这个方法不好用。Finder传来的视频,无论是否使用这个异步方法,时长都始终为0。

我不用Finder,采用照片应用进行调试,发现无论是不是用这个异步方法,都能获得正确的时长。

结合上一篇Finder分享视频到共享扩展的一些限制的小结。我产生了新的想法,经过测试,我的猜想是正确的。

那就是,AVMovie在读取URL的时候,会正确获得时长,如果是Data的话,则不可以。想测试也很简单,把URL变成Data,再创建AVMovie;或者把Data写入到URL,再创建AVMovie。就可以证实了。

我不清楚这个是苹果API的错误,还是特性。但是这个至少是没有注明的。

Finder分享视频到共享扩展的一些限制的小结

通常,我们使用NSItemProviderloadItem(forTypeIdentifier:options:completionHandler:)方法来加载对应类型的对象。但是Finder提供的信息有问题。不能使用推荐的类型来获得想要的对象。

比如分享mp4文件时,Finder提供的NSItemProvider是这样的。

<NSItemProvider: 0x6000000bfa40> {types = (
    "public.file-url",
    "public.url",
    "public.mpeg-4"
)}

你用"public.mpeg-4"作为类型来获取数据,不能获得视频文件对应的URL。这是很罕见的。因为大部分情况下,比如通过照片应用来获得视频时,都可以通过这种方式获得URL。

Finder提供给"public.mpeg-4"实际上是一个Data。但是这个Data用在这里实际上是有问题的。苹果不应该这么做。为什么呢?因为相比于其它类型的文件,比如图片或者声音之类的,视频文件可以是很大的。如果你要使用这个视频文件,那么通过Data来加载,就会占据大量的内存。实际上,苹果自己就在加载视频的文档中写到,不要全部加载到内存。

apple data warning

因为存在这个限制,Finder实际上传来的Data可能是不完整的。比如我尝试分享一个4GB的视频时,实际获得的Data的大小是415MB,并不完整。

结论,既然Data既占内存,又可能不完整,为什么Finder还要提供呢?只能说,苹果做错了。

临时的处理办法是这样的,先用"public.mpeg-4"尝试获取URL,如果获取失败。那么就看NSItemProvider是否支持URL。如果支持就再用"public.url"尝试获取URL。如果还不行,那就认为是没法处理。为什么不反过来呢?因为大部分的情况,都是第一种方式就能直接处理。Finder这么做实际上是错误的。但是为了适应它,才需要调整我们的代码。