[筆記] Swift深入淺出常見語法 – Grand Central Dispatch (GCD)

分享

基本用法

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)

DispatchSource

An object that coordinates the processing of specific low-level system events, such as file-system events, timers, and UNIX signals.

developer.apple.com/documentation

後續將引用其他實際案例來說明 ~

參考資料:

  1. Grand Central Dispatch Tutorial for Swift 4: Part 1/2
  2. Grand Central Dispatch Tutorial for Swift 4: Part 2/2

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *