在某些特殊情况下,我们需要使用延迟处理技术来规避一些系统的bug或者设计不当之处。这些任务在不采用延迟处理的情形之下,一般是无法完成的。
延迟处理的方式
延迟处理可用的方式有很多种,我推荐使用以下两种方式。
Timer
open class func scheduledTimer(timeInterval ti: TimeInterval, target aTarget: Any, selector aSelector: Selector, userInfo: Any?, repeats yesOrNo: Bool) -> Timer
@available(OSX 10.12, *)
open class func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Swift.Void) -> Timer
Timer
是最简单易用的方式。缺点是10.12之前的Timer
不支持block
,写起来比较麻烦,需要额外占用一个实例方法。
注意
下面的示例使用的Timer
,均使用非block
的形式。
GCD
public func asyncAfter(deadline: DispatchTime, qos: DispatchQoS = default, flags: DispatchWorkItemFlags = default, execute work: @escaping @convention(block) () -> Swift.Void)
public func asyncAfter(wallDeadline: DispatchWallTime, qos: DispatchQoS = default, flags: DispatchWorkItemFlags = default, execute work: @escaping @convention(block) () -> Swift.Void)
public func asyncAfter(deadline: DispatchTime, execute: DispatchWorkItem)
public func asyncAfter(wallDeadline: DispatchWallTime, execute: DispatchWorkItem)
DispatchTime
和DispatchWallTime
的区别是,前者是系统的启动时间(不包含系统睡眠时间),后者是挂钟的时间(也就是你在系统栏里看到的当前时间)。
注意
一般情况下,Timer
和GCD
的方式都是可以等价替换的。但是需要注意,Timer.scheduledTimer
运行在RunLoop.current
下,而不是main
。而GCD
,你可以指定是哪个队列。
延迟处理的技巧
规避UserDefaults的bug
当前的UserDefaults
存在didChangeNotification
在程序打开后意外发射的问题(rdar://28928098)。
考虑一下情形,在一个程序中,当你的偏好设置改变时,你希望视图控制器对应的视图也自动发生改变。一般这种情况下,需要偏好设置的控制器对于视图控制器保持一个弱的引用。当偏好发生改变时,偏好设置的控制器执行视图控制器的特定方法。特别的,如果你的程序先打开偏好设置,之后再打开一个视图控制器。此时,由于偏好设置是先打开的,而视图控制器是后打开的,必须视图控制器发送一个通知,由偏好设置的控制器接收这个通知,偏好设置的控制器才能正确的知道这个视图控制器。
但是由于上面提到的bug的存在。如果视图控制器直接发送通知,而同时系统又错误的发送了didChangeNotification
通知,就会导致特定的方法会一直执行,造成程序锁死。代码示例如下:
// in AppDelegate.swift
let TableViewControllerDidAppear = NSNotification.Name("TableViewController did Appear")
// in PreferencesViewController.swift
class PreferencesViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange), name: UserDefaults.didChangeNotification, object: UserDefaults.standard)
NotificationCenter.default.addObserver(self, selector: #selector(tableViewDidAppear(noti:)), name: TableViewControllerDidAppear, object: nil)
}
override func viewWillDisappear() {
super.viewWillDisappear()
NotificationCenter.default.removeObserver(self, name: UserDefaults.didChangeNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: TableViewControllerDidAppear, object: nil)
}
weak var tableViewController:TableViewController?
func userDefaultsDidChange() {
guard let controller = tableViewController else { return }
controller.run()
}
func tableViewDidAppear(noti:Notification) {
if let controller = noti.object as? TableViewController {
tableViewController = controller
}
}
}
// in TableViewController.swift
class TableViewController: NSViewController, NSTableViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
run()
NotificationCenter.default.post(name: TableViewControllerDidAppear, object: self)
}
func run() {
// do things
}
}
需要采用延迟的方式来发送这个通知。
Timer
的方式
class TableViewController: NSViewController, NSTableViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
run()
Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(sendNotification), userInfo: nil, repeats: false)
}
func sendNotification() {
NotificationCenter.default.post(name: TableViewControllerDidAppear, object: self)
}
func run() {
// do things
}
}
采用这种方式之后,即便UserDefaults
发出错误的通知,但是由于它发出通知时,偏好设置控制器中弱引用的视图控制器为nil
,因此不会造成锁死。
GCD
的方式
class TableViewController: NSViewController, NSTableViewDelegate {
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
run()
DispatchQueue.main.asyncAfter(wallDeadline: .now() + .milliseconds(100)) { [unowned self] () -> () in
NotificationCenter.default.post(name: TableViewControllerDidAppear, object: self)
}
}
func run() {
// do things
}
}
可以看出,GCD
的版本更为简练。
注意
wallDeadline
那里的计算方式很有趣,感兴趣的可以查一下。
规避Finder的不确定行为
大家知道我们可以使用AppDelegate
的
optional public func application(sender: NSApplication, openFiles filenames: [String])
方法来批量打开Finder
中的文件。但是,在实际使用中,你会发现,本该在一个窗口一次打开所有选中文件,却自动分成了多批,在多个窗口里打开。
这是因为,Finder
打开文件,会对文件进行分类,且这个分类和我们以为的不同,一般,我们会认为即便分类,也应该是相同的类型的分一类。但是Finder
的分类不是这样的。它会根据额外的信息来分类,这个信息是根据文件的来源判断的。比如你使用Safari
下载了一个txt的文本文件,Safari
会自动为这个文本文件添加来源信息,例如“下载至自某某网页”之类的。而有的下载工具下载时不会添加这个信息。Finder
将有和没有该信息的文件分为两组,这其实挺让人困惑的。
解决方案
var rawFilenames = [String]()
// instance counter
var timer:NSTimer! = nil
func application(sender: NSApplication, openFiles filenames: [String]) {
if shouldQuitAppAfterConvert == nil {
shouldQuitAppAfterConvert = true
}
if timer != nil {
timer.invalidate()
}
self.rawFilenames += filenames
timer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: #selector(prepareConvert), userInfo: nil, repeats: false)
}
func prepareConvert() {
// do something
}
先设定一个Timer
为延后0.1秒执行。此时如果func application(sender: NSApplication, openFiles filenames: [String])
方法被再次调用,则取消之前的Timer
,合并文件名,然后再设定一个新的Timer
,这样,就不会造成多窗口的打开了。
总结
当系统运行代码的顺序不符合我们的预期时,可以使用延迟处理的技术来改变代码运行的顺序。