Swift进阶 - Concurrency之GCD
如果看完了之前写的Swift初学中的文章,应该对swift的基础有了一定的掌握。现在我们讨论一下进阶一些的知识,当你写一个比较复杂的app时,需要经常进行网络请求,就需要好好的架构你的app,不要让你的app因为长时间的请求出现卡顿的情况。这时候就需要考虑app的Concurrency。
Swift提供了两个apis:GCD和Operations,其中Operation是以GCD为基础实现的。
一、GCD - Grand Central Dispatch
let queue = DispatchQueue(label: "com.test.test")
queue.async {
let a = 5 + 5
print(a)
DispatchQueue.main.async {
print("main\(a)")
}
}
gcd就是把你写在closure里的代码放到队列中,这个队列根据你给的参数,可以是serial(串行)也可以是concurrent(并发)的。serial queue只有一个thread,所以这里面的任务每次执行一个,这一个结束了才会执行下一个;concurrent queue就会根据资源分配多个threads,可以同时执行多个任务。注意以下几点:
1、GCD中的所有queue都是按照先进先出FIFO(first-in, first-out)的顺序,但是这指的是先进来的先执行,并不是先进来先完成,因为每个task的所需时间是不一样的;
2、当你执行queue.async的时候,你的代码也不一定会并发进行!如果你的queue是一个serial queue,只有一个thread,你在怎么async,它也只能一个一个执行。
3、label得是一个unique的string,你可以用“com.your-domain.xxx”的形式。
4、当你的app开始运行时,系统会自动创建一个主queue,就是例子中的DispatchQueue.main,这是serial queue,主要是用来呈现UI的。不要把一个sync任务推给main,会卡UI的
5、DispatchQueue默认会创建一个serial queue,你要这样定义来获取concurrent queue
let queue = DispatchQueue(label: "com.test.test", attributes: .concurrent)
6、Global concurrent queues
当你需要concurrent queue时,你可以用上面的语句来定义一个你自己的,但是通常情况下,用系统本来就提供的global concurrent queue就行了:
let queue = DispatchQueue.global(qos: .utility)
qos(Quality of service)是指你这个queue中的任务的优先级,一共有6中:
- .userInteractive - 这个任务与UI相关的时候,比如动画或者更新UI所需的逻辑运算等可以用这个,我们可不想因为复杂的运算影响UI的流畅性!
- .userInitiated - 这个是当用户触发了一个事件,我们需要立即执行相关逻辑任务,但这个任务又可以并发进行的时候,比如用户点了一个button
- .utility - 当你需要用一些progress bar或hud来显示加载中来执行此任务时,一般就是用这个了
- .background - 当这个任务用户完全不需要知道
- .default和.unspecified - 一般用不着,不讲了
简单例子
假设没有像SDWebImage的第三方库,你要写一个加载网络图片的uiCollectionView怎么写?这时候肯定要用到并发执行任务,不然肯定卡死(maybe not with 5g, but I don't care)
func loadImage(indexPath: IndexPath) {
let queue = DispatchQueue.global(qos: .utility) // a
queue.async {
[weak self] in // b
guard let self = self else { // c
return
}
if let data = try? Data(contentsOf: self.urls[indexPath.row]),
let image = UIImage(data: data) {
DispatchQueue.main.async {
if let cell = self.collectionView.cellForItem(at: indexPath)
as? PhotoCell { // d
cell.display(image: image)
}
}
}
}
}
a) 不知道图片都多大,所以也不知道加载一张图片需要多长时间,为了性能和电池,我们要用.utility
b) 这里在gcd的async直接用self也不会出现retain cycle,因为closure在执行完之后会释放内存,但self会被延长“寿命”。
c) 需要检查self是不是nil,万一加载出来之前self已经dismiss了。按照b里所说的,如果没用weak self,在加载出来前就不会dismiss
d) 在async里,我们不能直接传入cell,因为当async里的代码执行时,你并不知道这个cell的状态,有可能已经没了,也有可能被换了,所以我们要传入indexpath,然后获取实时的cell
7、Dispatch Group
假设你需要同时向服务器进行多次请求,只有这几个请求都完成时,你才能更新UI,这时候你就用DispatchGroup来解决:
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .utility)
queue.async(group: group) {
print(2^5)
}
queue.async(group: group) {
print(2^32)
}
group.notify(queue: DispatchQueue.main) {
print("finished")
finished只有在两个数都算完的时候才会被print。除了notify,group还有一个方法是wait - 如果用了group.wait(),它会把当前的queue给block,直到运行完group里的任务。wait还可以加一个timeout参数, 多少秒后,如果group中的任务还没执行完,那么正常继续执行当前queue后面的任务。举个例子就明白了
let group = DispatchGroup()
let queue = DispatchQueue.global(qos: .utility)
queue.async(group: group) {
Thread.sleep(until: Date().addingTimeInterval(2))
print("AAAAA")
}
queue.async(group: group) {
Thread.sleep(until: Date().addingTimeInterval(5))
print("BBBBBB")
}
if group.wait(timeout: .now()+3) == .timedOut {
print("不等了")
}
print("排队中...")
// AAAAA
// 不等了
// 排队中...
// BBBBBB
注意print的顺序!不要在main queue用group.wait()
8、@escaping
当func的其中一个参数是closure的时候,有两种情况:
a) 当func执行时会把closure的代码也执行,完成时,closure就不存在了
func sum(_ arr: [Int], handler: (Int) -> ()) {
print("doing sum...")
let total = arr.reduce(0, +)
handler(total)
print("finish")
}
sum([1, 2, 3]) {
total in
print(total)
}
// doing sum...
// 6
// finish
b) 当func执行完时,closure中的代码还没有执行完,这时候得让系统知道这个closure你要给我保存,不要在func执行完的时候把它从内存中毁掉,这时候就要加@escaping
func sum(_ arr: [Int], handler: @escaping (Int) -> ()) {
print("doing sum...")
let total = arr.reduce(0, +)
queue.async {
handler(total)
}
print("finish")
}
sum([1, 2, 3]) {
total in
Thread.sleep(until: Date().addingTimeInterval(1))
print(total)
}
// doing sum...
// finish
// 6
9、group.enter()和group.leave()
看完第7、8条后,再看看下面的代码,你觉得print出的顺序是怎样的?
func sum(_ arr: [Int], handler: @escaping (Int) -> ()) {
print("doing sum...")
let total = arr.reduce(0, +)
queue.async {
handler(total)
}
print("finish")
}
queue.async(group: group) {
sum(Array(1...2^32)) {
total in
Thread.sleep(until: Date().addingTimeInterval(2))
print("total: \(total)")
}
}
group.notify(queue: DispatchQueue.main) {
print("都完成了")
}
我们在第7条中知道group.notify会在所有在group中的queue里的任务都完成之后执行。但是上面这个例子会以一下顺序print
doing sum...
finish
都完成了
total: 595
因为queue执行的sum中还有一个async call,在这个async call还没完成的时候,notify就执行了!如果想解决这个问题,那么我们就可以用到group的两个方法:enter()和leave()。
func sum(_ arr: [Int], handler: @escaping (Int) -> ()) {
print("doing sum...")
let total = arr.reduce(0, +)
group.enter()
queue.async {
defer { group.leave() }
handler(total)
}
print("finish")
}
// doing sum...
// finish
// total: 595
// 都完成了
在执行sum里的async之前,call了enter()告诉group我还有代码没完成
a) 你要是call了enter()就要记得call leave()
b) 因为有时候你还需要handle error在async的结果中,所以用了defer为了不漏写leave
10、Semaphore
如果资源有限,想限制可以并发进行的任务,可以用semaphore,用法如下:
let semaphore = DispatchSemaphore(value: 2)
for i in 1...5 {
semaphore.wait()
queue.async(group: group) {
defer { semaphore.signal() }
print("start\(i)")
Thread.sleep(until: Date().addingTimeInterval(1))
print("end\(i)")
}
}
group.notify(queue: DispatchQueue.main) {
print("all finished")
}
// print结果
start1
start2
end1
end2
start3
start4
end4
end3
start5
end5
all finished