內容目錄
基本用法
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
// 在Main Thread非同步延遲2秒
guard let self = self else {
return
}
// 顯示特定字串於NavigationItem.prompt
self.navigationItem.prompt = "Add photos with faces to Googlyify them!"
// 強制NavigationBar刷新Layout
self.navigationController?.viewIfLoaded?.setNeedsLayout()
}
為什麼不使用 Timer
?使用Timer時,需要額外定義Method並透過 Selector
來調用,DispatchQueue
僅需要 asyncAfter
的閉包即可完成。另外,Timer必須確保它在正確的循環中運行。因此調用 DispatchQueue
會更簡單一些!
管理 Singletons (處理讀寫問題: 讀寫鎖)
class PhotoManager {
private init() {}
static let shared = PhotoManager()
// 設置label參數有助於之後Debug
private let concurrentPhotoQueue =
DispatchQueue(
label: "com.raywenderlich.GooglyPuff.photoQueue",
attributes: .concurrent)
private var unsafePhotos: [Photo] = []
var photos: [Photo] {
var photosCopy: [Photo] = []
// 使用sync來追蹤queue中的dispatch barrier
concurrentPhotoQueue.sync {
photosCopy = self.unsafePhotos
}
return photosCopy
}
func addPhoto(_ photo: Photo) {
// 設置flags: .barrier來進行read/write lock,讓concurrentPhotoQueue進入serial狀態,直到完成任務
concurrentPhotoQueue.async(flags: .barrier) { [weak self] in
guard let self = self else {
return
}
self.unsafePhotos.append(photo)
}
}
}
多任務處理、結束與取消(DispatchGroup)
func downloadPhotos(withCompletion completion: BatchPhotoDownloadingCompletionClosure?) {
var storedError: NSError?
// 定義一個DispatchGroup
let downloadGroup = DispatchGroup()
var addresses = [PhotoURLString.overlyAttachedGirlfriend,
PhotoURLString.successKid,
PhotoURLString.lotsOfFaces]
var blocks: [DispatchWorkItem] = []
for index in 0..<addresses.count {
// 每個任務執行前
downloadGroup.enter()
// 使用DispatchWorkItem來管理任務(取消任務)
let block = DispatchWorkItem(flags: .inheritQoS) {
let address = addresses[index]
let url = URL(string: address)
let photo = DownloadPhoto(url: url!) { _, error in
if error != nil {
storedError = error
}
// 每個任務結束後
downloadGroup.leave()
}
PhotoManager.shared.addPhoto(photo)
}
blocks.append(block)
DispatchQueue.main.async(execute: block)
}
for block in blocks[0..<blocks.count] {
let cancel = Bool.random()
if cancel {
// 取消特定任務
block.cancel()
// 要記得.leave()喔
downloadGroup.leave()
}
}
// 判斷任務結束:
// 方法一: .wait()
// downloadGroup.wait()
// DispatchQueue.main.async {
// completion?(storedError)
// }
// 方法二: .notify()
downloadGroup.notify(queue: DispatchQueue.main) {
completion?(storedError)
}
}
除了用 for
迴圈外,DispatchQueue
也提供了 concurrentPerform
的函式來執行多任務:
// ... 略
let _ = DispatchQueue.global(qos: .userInitiated)
DispatchQueue.concurrentPerform(iterations: addresses.count) { index in
let address = addresses[index]
let url = URL(string: address)
downloadGroup.enter()
let photo = DownloadPhoto(url: url!) { _, error in
if error != nil {
storedError = error
}
downloadGroup.leave()
}
PhotoManager.shared.addPhoto(photo)
}
downloadGroup.notify(queue: DispatchQueue.main) {
completion?(storedError)
}
Unit Test (DispatchSemaphore)
private let defaultTimeoutLengthInSeconds: Int = 10 // 10 Seconds
class GooglyPuffTests: XCTestCase {
func testLotsOfFacesImageURL() {
downloadImageURL(withString: PhotoURLString.lotsOfFaces)
}
func testSuccessKidImageURL() {
downloadImageURL(withString: PhotoURLString.successKid)
}
func testOverlyAttachedGirlfriendImageURL() {
downloadImageURL(withString: PhotoURLString.overlyAttachedGirlfriend)
}
func downloadImageURL(withString urlString: String) {
let url = URL(string: urlString)
// 預設訊號量為: 0
let semaphore = DispatchSemaphore(value: 0)
let _ = DownloadPhoto(url: url!) { _, error in
if let error = error {
XCTFail("\(urlString) failed. \(error.localizedDescription)")
}
// .signal() 會讓訊號量 + 1
semaphore.signal()
}
// timeout = 10秒
let timeout = DispatchTime.now() + .seconds(defaultTimeoutLengthInSeconds)
// semaphore會緩衝10秒
if semaphore.wait(timeout: timeout) == .timedOut {
如果10秒內訊號量沒有變化(0 -> 1),測試就會Fail
XCTFail("\(urlString) timed out")
}
}
}
其他用法(DispatchSource)
後續將引用其他實際案例來說明 ~