GCD和延时调用
在下面我给出了一个日常里最通常会使用到的例子(说这个例子能覆盖到日常的GCD使用的50%以上也不为过),来展示一下Swift里的GCD调用会是什么样子:
// MARK: - GCD
// 创建目标队列
let workingQueue = DispatchQueue(label: "MyQueue")
// 派发到刚创建的队列中
workingQueue.async {
// 在 workingQueue 中异步进行
print("努力工作")
Thread.sleep(forTimeInterval: 2) //模拟两秒的执行时间
DispatchQueue.main.async {
// 返回到主线程更新 UI
print("结束工作, 更新 UI")
}
}
因为 UIKit
是只能在主线程工作的,如果我们在主线程进行繁重的工作的话,就会导致 app 出现“卡死”的现象:UI不能更新,用户输入无法响应等等,是非常糟糕的用户体验。为了避免这种情况的出现,对于繁重(如图像加滤镜等)或会很长时间才能完成的(如从网络下载图片)处理,我们 应该把它们放到后台线程进行,这样在用户看来UI还是可以交互的,也不会出现卡顿。在工作进行完成后,我们需要更新UI的话,必须回到主线程进行(牢记UI相关的工作都需要在主线程执行,否则可能发生不可预知的错误)。
GCD里有一个很好用的延时调用,那就是asyncAfter
。最简单的使用方法看起来是这样的:
// MARK: - 延时调用
let time: TimeInterval = 2.0
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + time) {
print("2 秒后输出")
}
在这里我们可以稍微将它封装的好用一些,最好再加上取消的功能。在iOS 8中GCD得到了惊人的进化,现在我们可以通过将一个闭包封装到 DispatchworkItem
对象中,然后对其发送 cancle
,来取消一个正在等待执行的 block
。取消一个任务这样的特性,这在以前是 NSOperation
的专利,但是现在我们使用 GCD 也能达到同样的目的了。这里我们不使用这个方式,而是通过捕获一个 cancel
标识变量来实现 delay call 的取消,整个封装也许有点长,但我还是推荐一读。
import Foundation
typealias Task = (_ cancle: Bool) -> Void
func delay(_ time: TimeInterval, task: @escaping () -> ()) -> Task? {
// 嵌套函数
func dispatch_later(block: @escaping () -> ()) {
let t = DispatchTime.now() + time
DispatchQueue.main.asyncAfter(deadline: t, execute: block)
}
var closure: (() -> Void)? = task
var result: Task?
let delayedClosure: Task = {
cancel in
if let internalColsure = closure {
if cancel == false {
DispatchQueue.main.async(execute: internalColsure)
}
}
closure = nil
result = nil
}
result = delayedClosure
dispatch_later {
if let delayedClosure = result {
delayedClosure(false)
}
}
return result
}
func cancel(_ task: Task?) {
task?(true)
}
使用的时候就很简单了, 我们想在2秒之后干点啥的话:
delay(2) { print("2 秒后输出") }
想要取消的话,我们可以保留一个队 Task
的引用,然后调用 cancel
:
let task = delay(5) {
print("拨打 110")
}
//仔细想想还是取消为妙
cancel(task)
Lock
无并发,不编码。而只要一说到多线程或者并发的代码,我们可能就很难绕开对于锁的讨论。简单来说,为了在不同线程中安全地访问同一个资源,我们需要这些访问顺序进行。Cocoa和Objective-C中加锁的方式有很多,但是其中在日常开发中最常用的应该是 @syncchronized
,这个关键字可以用来修饰一个变量,并为其自动加上和解除互斥锁。这样,可以保证变量在作用范围内不会被其他线程改变。举个例子,如果我们有一个方法接受参数,需要这个方法是线程安全的话,就需要在参数上加锁:
- (void)myMethod:(id)anObj {
@synchronized (anObj) {
// 在括号内 anObj 不会被其他线程改变
}
}
如果没有锁的话,一旦 anObj
的内容被其他线程修改的话,这个方法的行为很可能就无法预测了。
但是加锁和解锁都是要消耗一定性能的,因此我们不太可能为所有的方法都加上锁。另外其实在 一个app中可能会涉及到多线程的部分是有限的,我们也没有必要为所有东西加上锁。过多的锁不仅没有意义,而且对于多线程编程来说,可能会产生很多像死锁这样的陷阱,也难以调试。因 此在使用多线程时,我们应该尽量将保持简单作为第一要务。
扯远了,我们回到 @synchronized
上来。虽然这个方法很简单好用,但是很不幸的是在Swift中它已经(或者是暂时)不存在了。其实@synchronized
在幕后做的事情是调用了 objc_sync
中的 objc_sync_enter
和 objc_sync_exit
方法,并且加入了一些异常判断。因此,在Swift中,如果我们忽略掉那些异常的话,我们想要 lock 一个变量的话,可以这样写:
func MyMethod(anObj: Any!) {
objc_sync_enter(anObj)
// 在 enter 和 exit 之间 anObj 不会被其他线程改变
objc_sync_exit(anObj)
}
更进一步,如果我们喜欢以前的那种形式,甚至可以写一个全局的方法,并接受一个闭包,来将objc_sync_enter
和 objc_sync_exit
封装起来:
func synchronized(_ lock: Any, closure:()->()) {
objc_sync_enter(lock)
closure()
objc_sync_exit(lock)
}
再结合 Swift 的尾随闭包的语言特性, 这样, 使用起来就和 Objective-C 中很像了:
func myMethodLocked(anObj: Any!) {
synchronized(anObj) {
// 在 括号内 anObj 不会被其他线程改变
}
}