iOS多執行緒程式設計三:Operation和OperationQueue

RiverLi發表於2019-03-01

概述

本文是多執行緒的第三篇文章,主要講解Operation和OperationQueue。看資料的時候發現了一篇特別好的教程,隨性就翻譯一下,權當自己做個總結。本文主要內容翻譯自raywenderlichoperation and operationqueue tutorial in swift。原文連結點這裡

不管是在Mac還是iOS系統上,當使用者使用你的app點選按鈕或者編輯文字的時候突然你的app卡死,介面變得無法響應,這對使用者來講是非常糟糕的體驗。 在Mac系統上使用者不得不盯著彩色的載入框等待恢復。在iOS系統上連載入框都看不到。使用者希望在他們觸控螢幕之後應用程式能立即做出響應。卡頓的app讓使用者感到這個app很笨重,反應太慢。這會造成使用者在AppStore中給應用差評。

保持app的流暢響應說起來容易做起來可是相當不容易。一旦你的app需要處理多個任務,事情很快變得複雜起來。主執行緒的run loop迴圈中沒有足夠的時間來處理繁重的任務,同時還要響應使用者的UI操作。

這種情況下,開發者該如何應對呢?解決辦法就是通過多並行的手段將任務從主執行緒中剝離。並行的意思是你的應用程式同時執行多個工作流(或者多個執行緒)。這使得在執行任務的同時使用者介面同時能夠保持響應。

OperationOPerationQueue這兩個類是iOS系統中併發執行任務的一種方式。本教程就是講述的就是如何使用它們。我們將以一個沒用併發的例子開始,它會非常的卡頓,然後你將逐步加入併發操作,使之變得流暢。

開始

我們的例子是展示一個滾動的圖片列表,圖片來自於網路,然後下載圖片之後需要對其過濾,然後新增到表格上顯示。應用的模型如下:

應用模型

第一次嘗試

點選這裡下載最初的專案,這是該教程專案的第一個版本。

注意: 所有的圖片都來自stock.xchng 資料來源中有些的圖片名字被故意錯誤的命名,這是為了模擬圖片下載失敗的場景。

執行該專案,你講看到滾動的圖片列表如下,滑動該列表,是不是很不流暢。(國內需要給手機翻牆或者掛代理)。

執行效果

所有的響應程式碼都在ListViewController.swift這個類中,大部分在**tableView(_:cellForRowAtIndexPath:**裡。

檢視該方法裡的程式碼,可以發現這裡主要做了兩件事:

  • 從網路上下載圖片。
  • 使用Core Image 過濾圖片。

還有,第一次載入的時候,你需要從網路上獲取所有圖片的url。

lazy var photos = NSDictionary(contentsOf:dataSourceURL)!
複製程式碼

所有這些工作都發生在應用程式的主執行緒。由於主執行緒還擔任著介面的互動,過多的在主執行緒上載入圖片,過濾圖片會給主執行緒造成壓力,這必然會犧牲應用的響應流暢度。你可以在Xcode的debug 導航條上檢視CPU的工作情況。

cpu執行情況

從圖中我們可以看到,大部分的任務消耗在thread1, 它是應用程式的主執行緒。

接下來我們來優化使用者體驗。

任務,執行緒,程式

在進行下一步之前,有幾個概念你需要了解一下:

  • 任務:你需要處理的一個簡單的,獨立的工作。
  • 執行緒:由作業系統提供的一種機制,可以使在一個app內讓多個操作同時執行。
  • 程式:一個可執行的程式碼塊,由多個執行緒組成。

在iOS和MacOS系統上,執行緒的功能是由POSIX執行緒API(或者pthreads)提供的,它們是作業系統的一部分。它們是很底層的,使用這些ÅPI編寫程式碼及易出錯,更糟糕的是由它們引發的錯誤非常難排查

Foundation框架包含了一個Thread的類,它是執行緒相對容易使用,但使用Thread管理多執行緒仍然令人頭疼。Operation 和 OperationQueue 是更高層次的API,使用它們處理多執行緒非常的方便了很多。

下面展示了程式、執行緒、任務之間的關係。

程式、執行緒、任務之間的關係

如上圖所示,一個程式和包含多個執行緒,同一時刻每個執行緒可以同時執行多個任務。

在上圖中,Thread2 執行讀取檔案的操作,同時Thread1執行使用者互動相關的程式碼。這和iOS程式碼結構十分相似:主執行緒應該執行和使用者互動相關的操作,其他執行緒應該執行耗時操作,如讀取檔案,訪問網路等等。

Operation 和 GCD的比較

你可能聽說過GCD,簡而言之,GCD由語言功能,執行時庫和系統增強功能組成,可提供系統和全面的改進,以支援iOS和macOS中多核硬體的併發性。如果你想學習GCD可以參考GCD Tutorial

OperationOperationQueue 是構建在GCD之上的,通常來說,蘋果建議使用最高階別的API來開發,必要的時候再使用低階API。

下面是這兩套API的簡單比較,以便幫助你決定什麼時間什麼地方使用GCD或者OPeration:

  • GCD 是一種輕量級的方式,用以表示即將並行執行的工作單元。你不用負責這些工作單元的執行,作業系統替你負責工作單元的排程。新增block之間的依賴是一件令人頭疼的事,取消或者掛起一個block需要你編寫額外的程式碼。

  • GCD操作增加了一些額外的開銷,但您可以在各種操作之間新增依賴關係並重新使用,取消或暫停它們。

這篇教程使用Operation,因為你處理的是tableView,出於效能的原因,你需要在圖片離開螢幕的時候取消圖片的下載任務。即使這些操作實在後臺執行緒中執行的,但如果大量的操作在佇列中等待,效能依然會很糟糕。

重新定義App模型

是時候重新定義最初的沒有額外執行緒的模型了。如果你仔細觀察了最初的模型,你會發現它有三個地方可以提高。將這三個地方放在單獨的執行緒中,主執行緒就可以安心的響應使用者介面了。

重新定義模型

為了擺脫App的效能瓶頸,你需要一個執行緒來專門響應使用者操作,一個執行緒來資料來源和圖片,一個執行緒來執行圖片過濾。在新的模型中,App從主執行緒中啟動,並載入一個空的tableView,同時,app開啟一個執行緒來載入資料來源。

一旦資料來源下載成功,你講告知tableView重新整理表格,重新整理表格的操作必須在主執行緒中執行,因為它涉及到介面操作。這時,tableView知道了它擁有多少個行和需要展示的圖片的URL,但它還沒有真實的圖片,如果這時你立即開始下載所有的圖片,這將是非常糟糕的,因為你不需要同時展示所有的圖片。

那麼,怎麼做會好一點呢?

一個比較好的方式是僅僅下載那些螢幕內可見的cell的圖片。因此你的程式碼應首先詢問tableView那些行是可見的,然後再開啟下載任務。與之相似,圖片過濾操作也不能等待圖片完全下載之後開始。因此,應用程式不應啟動影象過濾任務,直到有未經過濾的影象等待處理。

為了使app看起來更加可響應,一旦圖片下載之後就會展示它。然後開始圖片過濾,然後將過濾後的圖片顯示在介面上。下圖展示了新模型的排程控制情況:

控制流程

為了實現這些目標,你需要跟蹤image的狀態,下載中,已下載,已過濾。你也需要跟蹤每個operation的狀態和型別,以便於你在使用者滾動的時候可以取消,暫停或者恢復它。

好啦,現在開始編碼!

開啟Xcode,新建一個Swift檔案,叫做PhotoOperations.swift ,新增如下程式碼:

import UIKit

// This enum contains all the possible states a photo record can be in
enum PhotoRecordState {
  case new, downloaded, filtered, failed
}

class PhotoRecord {
  let name: String
  let url: URL
  var state = PhotoRecordState.new
  var image = UIImage(named: "Placeholder")
  
  init(name:String, url:URL) {
    self.name = name
    self.url = url
  }
}
複製程式碼

這個類代表了app內展示的每個圖片和它當前的狀態,預設是 .new,圖片預設情況下是一個佔點陣圖。

為了追蹤每個opration的狀態,你需要另一個類,在photoOperations.swift 的末尾新增如下類:

class PendingOperations {
  lazy var downloadsInProgress: [IndexPath: Operation] = [:]
  lazy var downloadQueue: OperationQueue = {
    var queue = OperationQueue()
    queue.name = "Download queue"
    queue.maxConcurrentOperationCount = 1
    return queue
  }()
  
  lazy var filtrationsInProgress: [IndexPath: Operation] = [:]
  lazy var filtrationQueue: OperationQueue = {
    var queue = OperationQueue()
    queue.name = "Image Filtration queue"
    queue.maxConcurrentOperationCount = 1
    return queue
  }()
}
複製程式碼

此類包含兩個字典,用於跟蹤表中每行的活動和掛起下載和過濾操作,以及每種操作型別的操作佇列。

所有的操作都是懶載入,他們到第一次訪問的時候才初始化,這會提升app的效能。

如你所見,建立一個OperationQueue非常簡單,對queue進行命名有助於你除錯程式碼。最大併發運算元量設定為1是為了教學所有,為了讓你看到操作被一個接一個的完成。您可以將此部分保留,並允許佇列決定它可以同時處理多少操作 - 這將進一步提高效能。

佇列如何決定一次可以執行多少個操作?這是個好問題!這取決於硬體。預設情況下,OperationQueue會在幕後進行一些計算,確定它執行的特定平臺的最佳值,並啟動儘可能多的執行緒。

考慮如下情況:假設系統處於空閒狀態,並且有大量可用資源。在這種情況下,佇列可以同時啟動八個執行緒。下次執行程式時,系統可能正忙於其他消耗資源操作。這次,佇列可能只啟動兩個同時發生的執行緒。因為在此應用程式中設定了最大併發操作計數,所以一次只能執行一個操作。

你可能會好奇為什麼要跟蹤所有活動和掛起的oprations呢?queue有一個operations方法,這個方法返回operation的陣列,為什麼不利用他呢? 在這個專案中,因為他的效率並不高。你需要跟蹤那個Operation和tableView的row相關,這將涉及到每次使用的時候遍歷陣列。但把他們儲存在一個字典裡,並使用Indexpath作為key,意味著查詢的時候將更加快和高效。

是時候關心下載和過濾的操作了,將下面程式碼新增到PhotoOperations.swift 的尾部。

class ImageDownloader: Operation {
  //1
  let photoRecord: PhotoRecord
  
  //2
  init(_ photoRecord: PhotoRecord) {
    self.photoRecord = photoRecord
  }
  
  //3
  override func main() {
    //4
    if isCancelled {
      return
    }

    //5
    guard let imageData = try? Data(contentsOf: photoRecord.url) else { return }
    
    //6
    if isCancelled {
      return
    }
    
    //7
    if !imageData.isEmpty {
      photoRecord.image = UIImage(data:imageData)
      photoRecord.state = .downloaded
    } else {
      photoRecord.state = .failed
      photoRecord.image = UIImage(named: "Failed")
    }
  }
}
複製程式碼

Operation是一個抽象類,專為子類化而設計。每個子類代表一個特定的任務,如前面的圖所示。

以下是上述程式碼中每個編號註釋的內容:

  1. 新增一個常量,引用與operation相關的PhotoRecord。
  2. 建立初始化方法,並傳入PhotoRecord。
  3. main() 方法是重寫自operation, 是真正執行任務的地方。
  4. 在開始之前檢查是否處於取消狀態。在嘗試耗時的操作之前,應定期檢查是否處於取消狀態。
  5. 下載圖片資料。
  6. 重新檢測是否處於取消狀態。
  7. 如果圖片成功下載,建立圖片並新增到record中,然後設定狀態。如果下載失敗,標記record為失敗,並設定相應的圖片。

下一步,你需要建立另一個operation 來處理圖片過濾。把下面的程式碼新增到PhotoOperations.swift的尾部。

class ImageFiltration: Operation {
  let photoRecord: PhotoRecord
  
  init(_ photoRecord: PhotoRecord) {
    self.photoRecord = photoRecord
  }
  
  override func main () {
    if isCancelled {
        return
    }
      
    guard self.photoRecord.state == .downloaded else {
      return
    }
      
    if let image = photoRecord.image, 
       let filteredImage = applySepiaFilter(image) {
      photoRecord.image = filteredImage
      photoRecord.state = .filtered
    }
  }
}
複製程式碼

這看起來與下載操作非常相似,只是使用了影象過濾操作(使用尚未實現的方法,因此編譯器錯誤)替換了下載操作。

在ImageFiltration類中新增過濾方法

func applySepiaFilter(_ image: UIImage) -> UIImage? {
  guard let data = UIImagePNGRepresentation(image) else { return nil }
  let inputImage = CIImage(data: data)
      
  if isCancelled {
    return nil
  }
      
  let context = CIContext(options: nil)
      
  guard let filter = CIFilter(name: "CISepiaTone") else { return nil }
  filter.setValue(inputImage, forKey: kCIInputImageKey)
  filter.setValue(0.8, forKey: "inputIntensity")
      
  if isCancelled {
    return nil
  }
      
  guard 
    let outputImage = filter.outputImage,
    let outImage = context.createCGImage(outputImage, from: outputImage.extent) 
  else {
    return nil
  }

  return UIImage(cgImage: outImage)
}
複製程式碼

這個方法和之前ListViewController中提供的過濾方法基本一致,移到這裡是為了讓其可以在operation中執行。同樣的你需要頻繁的檢查operation的是否處於取消狀態。一旦你的過濾執行完畢,需要重置record中的值。

到這裡我們已經有了所有的工具和方法來在後臺處理任務。現在,我們回到viewcontroller來修改程式碼以提高效能。

切換到ListViewController.swift檔案,刪除lazy var photos 屬性,新增如下程式碼:

var photos: [PhotoRecord] = []
let pendingOperations = PendingOperations()
複製程式碼

這兩個屬相持有了PhotoRecord組成的陣列和PendingOperations物件來管理operations。

新增一個新的方法來下載photos陣列的值

func fetchPhotoDetails() {
  let request = URLRequest(url: dataSourceURL)
  UIApplication.shared.isNetworkActivityIndicatorVisible = true

  // 1
  let task = URLSession(configuration: .default).dataTask(with: request) { data, response, error in

    // 2
    let alertController = UIAlertController(title: "Oops!",
                                            message: "There was an error fetching photo details.",
                                            preferredStyle: .alert)
    let okAction = UIAlertAction(title: "OK", style: .default)
    alertController.addAction(okAction)

    if let data = data {
      do {
        // 3
        let datasourceDictionary =
          try PropertyListSerialization.propertyList(from: data,
                                                     options: [],
                                                     format: nil) as! [String: String]

        // 4
        for (name, value) in datasourceDictionary {
          let url = URL(string: value)
          if let url = url {
            let photoRecord = PhotoRecord(name: name, url: url)
            self.photos.append(photoRecord)
          }
        }

        // 5
        DispatchQueue.main.async {
          UIApplication.shared.isNetworkActivityIndicatorVisible = false
          self.tableView.reloadData()
        }
        // 6
      } catch {
        DispatchQueue.main.async {
          self.present(alertController, animated: true, completion: nil)
        }
      }
    }

    // 6
    if error != nil {
      DispatchQueue.main.async {
        UIApplication.shared.isNetworkActivityIndicatorVisible = false
        self.present(alertController, animated: true, completion: nil)
      }
    }
  }
  // 7
  task.resume()
}
複製程式碼
  1. 建立URLSession任務在後臺下載image列表。
  2. 配置UIAlertController,在錯誤的時候使用它。
  3. 如果請求成功,請從屬性列表中建立字典。字典使用影象名稱作為鍵,其URL作為值。
  4. 從字典構建PhotoRecord物件陣列。
  5. 返回主執行緒以重新載入表檢視並顯示影象。
  6. 發生錯誤時顯示警告控制器。請記住,URLSession任務在後臺執行緒上執行,並且必須在主執行緒中顯示螢幕上的任何訊息。
  7. 執行下載任務。

在viewDidLoad()方法中呼叫這個方法:

fetchPhotoDetails()
複製程式碼

下一步,找到tableView(_:cellForRowAtIndexPath:)方法,用以下程式碼替換它

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  let cell = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier", for: indexPath)
  
  //1
  if cell.accessoryView == nil {
    let indicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
    cell.accessoryView = indicator
  }
  let indicator = cell.accessoryView as! UIActivityIndicatorView
  
  //2
  let photoDetails = photos[indexPath.row]
  
  //3
  cell.textLabel?.text = photoDetails.name
  cell.imageView?.image = photoDetails.image
  
  //4
  switch (photoDetails.state) {
  case .filtered:
    indicator.stopAnimating()
  case .failed:
    indicator.stopAnimating()
    cell.textLabel?.text = "Failed to load"
  case .new, .downloaded:
    indicator.startAnimating()
    startOperations(for: photoDetails, at: indexPath)
  }
  
  return cell
}
複製程式碼
  1. 建立UIActivityIndicatorView並將其設定為單元的附件檢視, 為提供反饋。
  2. 根據indexpath從資料來源中取出PhotoRecord。
  3. 單元格的文字標籤(幾乎)始終相同,PhotoRecord的圖片在狀態變更後設定,因此無論record的狀態如何,您都可以在此處設定它們。
  4. 檢查Record。根據需要設定活動指示符和文字,然後啟動操作(尚未實現)。

實現啟動操作方法

func startOperations(for photoRecord: PhotoRecord, at indexPath: IndexPath) {
  switch (photoRecord.state) {
  case .new:
    startDownload(for: photoRecord, at: indexPath)
  case .downloaded:
    startFiltration(for: photoRecord, at: indexPath)
  default:
    NSLog("do nothing")
  }
}
複製程式碼

在這裡,您傳入PhotoRecord的例項及其索引路徑。根據照片記錄的狀態,您可以啟動下載或過濾操作。

下載和過濾影象的方法是分開實現的。因為有可能在下載影象時使用者可以滾動,所以你並不需要應用影象過濾器。下次使用者來到同一行時,您無需重新下載影象;你只需要應用影象過濾器!

現在,您需要實現在上面的方法中呼叫的方法。前面我們建立了一個自定義類PendingOperations來跟蹤操作; 現在你真的開始使用它了!將以下方法新增到類中:

func startDownload(for photoRecord: PhotoRecord, at indexPath: IndexPath) {
  //1
  guard pendingOperations.downloadsInProgress[indexPath] == nil else {
    return
  }
      
  //2
  let downloader = ImageDownloader(photoRecord)
  
  //3
  downloader.completionBlock = {
    if downloader.isCancelled {
      return
    }

    DispatchQueue.main.async {
      self.pendingOperations.downloadsInProgress.removeValue(forKey: indexPath)
      self.tableView.reloadRows(at: [indexPath], with: .fade)
    }
  }
  
  //4
  pendingOperations.downloadsInProgress[indexPath] = downloader
  
  //5
  pendingOperations.downloadQueue.addOperation(downloader)
}
    
func startFiltration(for photoRecord: PhotoRecord, at indexPath: IndexPath) {
  guard pendingOperations.filtrationsInProgress[indexPath] == nil else {
      return
  }
      
  let filterer = ImageFiltration(photoRecord)
  filterer.completionBlock = {
    if filterer.isCancelled {
      return
    }
    
    DispatchQueue.main.async {
      self.pendingOperations.filtrationsInProgress.removeValue(forKey: indexPath)
      self.tableView.reloadRows(at: [indexPath], with: .fade)
    }
  }
  
  pendingOperations.filtrationsInProgress[indexPath] = filterer
  pendingOperations.filtrationQueue.addOperation(filterer)
}
複製程式碼
  1. 從downloadsInProgress中查詢指定的IndexPath中是否有operation,如果有就返回。
  2. 如果沒有就建立ImageDownloader。
  3. 新增一個完成的block,它將在operation執行完畢之後執行。這是讓您的應用程式的其餘部分知道操作已完成的好地方。要注意如果操作被取消也會執行完成塊,因此您必須在執行任何操作之前檢查此屬性。你也無法保證呼叫完成塊的執行緒,因此需要使用GCD觸發主執行緒上表檢視的重新載入。
  4. 將operation新增到downloadsInProgress以幫助跟蹤。
  5. 將操作新增到下載佇列。這是開始執行任務的方式。一旦你將operation新增到佇列,佇列開始處理任務的排程。

startFiltration 方法遵循同樣的原則,只是使用了ImageFiltration和filtrationsInProgress來追蹤operation。

到這裡,我們已經完成了。執行該專案,檢視效果。你滾動瀏覽表格檢視,應用程式不再停止並開始下載影象並在它們變得可見時對其進行過濾。

效果

是不是很酷?你可以看到一點點努力可以大大提高您的應用程式響應速度 - 併為使用者帶來更多樂趣!

微調

你在本教程中已經走了很長的路!你的小專案具有響應性,並且與原始版本相比有很多改進。但是,仍有一些小細節需要處理。

你可能已經注意到,當你在表格檢視中滾動時,這些螢幕外單元格仍處於下載和過濾的過程中。如果您快速滾動,應用程式將忙於下載並過濾列表中更靠後的單元格中的影象,即使它們不可見。理想情況下,應用程式應取消對螢幕外單元格的過濾,並優先顯示當前顯示的單元格。

你可以在你的程式碼中新增取消規則,來進一步優化。

開啟 ListViewController.swift 找tableView(_:cellForRowAtIndexPath:) 方法,在呼叫 startOperationsForPhotoRecord 方法的地方新增判斷

if !tableView.isDragging && !tableView.isDecelerating {
  startOperations(for: photoDetails, at: indexPath)
}
複製程式碼

只有在表檢視不滾動時,才能告訴表檢視啟動操作。這些實際上是UIScrollView的屬性,因為UITableView是UIScrollView的子類,所以表檢視會自動繼承這些屬性。

接下來,將以下UIScrollView委託方法的實現新增到類中:

override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  //1
  suspendAllOperations()
}

override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
  // 2
  if !decelerate {
    loadImagesForOnscreenCells()
    resumeAllOperations()
  }
}

override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  // 3
  loadImagesForOnscreenCells()
  resumeAllOperations()
}
複製程式碼

快速瀏覽上面的程式碼顯示以下內容:

  1. 在使用者開始滾動時,你將希望暫停所有操作並檢視使用者想要檢視的內容。您將在稍後實現suspendAllOperations。
  2. 如果decelerate的值為false,則表示使用者停止拖動表檢視。因此,你希望恢復暫停操作,取消螢幕外單元格的操作,以及啟動螢幕單元格的操作。之後實現loadImagesForOnscreenCells和resumeAllOperations。
  3. 此委託方法告訴你表檢視停止滾動,因此需要執行與操作2相同的操作。

接下來,將下面方法新增到ListViewController中。

func suspendAllOperations() {
  pendingOperations.downloadQueue.isSuspended = true
  pendingOperations.filtrationQueue.isSuspended = true
}

func resumeAllOperations() {
  pendingOperations.downloadQueue.isSuspended = false
  pendingOperations.filtrationQueue.isSuspended = false
}

func loadImagesForOnscreenCells() {
  //1
  if let pathsArray = tableView.indexPathsForVisibleRows {
    //2
    var allPendingOperations = Set(pendingOperations.downloadsInProgress.keys)
    allPendingOperations.formUnion(pendingOperations.filtrationsInProgress.keys)
      
    //3
    var toBeCancelled = allPendingOperations
    let visiblePaths = Set(pathsArray)
    toBeCancelled.subtract(visiblePaths)
      
    //4
    var toBeStarted = visiblePaths
    toBeStarted.subtract(allPendingOperations)
      
    // 5
    for indexPath in toBeCancelled {
      if let pendingDownload = pendingOperations.downloadsInProgress[indexPath] {
        pendingDownload.cancel()
      }
      pendingOperations.downloadsInProgress.removeValue(forKey: indexPath)
      if let pendingFiltration = pendingOperations.filtrationsInProgress[indexPath] {
        pendingFiltration.cancel()
      }
      pendingOperations.filtrationsInProgress.removeValue(forKey: indexPath)
    }
      
    // 6
    for indexPath in toBeStarted {
      let recordToProcess = photos[indexPath.row]
      startOperations(for: recordToProcess, at: indexPath)
    }
  }
}
複製程式碼

suspendAllOperations() and resumeAllOperations() 實現起來非常簡單,OperationQueue可以通過將suspended屬性設定為true來掛起queque裡面的所有的操作。

loadImagesForOnscreenCells() 有一點複雜。

  1. 從包含表檢視中所有當前可見行的索引路徑的陣列開始。
  2. 過組合正在進行的所有下載和正在進行的所有過濾器,構建一組所有待處理操作。
  3. 構造一組包含要取消的操作的索引路徑。從所有操作開始,然後刪除可見行的索引路徑。這將使一組操作涉及螢幕外行。
  4. 構造一組需要啟動其操作的索引路徑。從索引路徑開始所有可見行,然後刪除操作已掛起的那些行。
  5. 迴圈訪問要取消的那些,取消它們,並從PendingOperations中刪除它們的引用。
  6. 遍歷那些要啟動的,併為每個呼叫startOperations(for:at :)。

執行應用,這是一個響應更快,資源管理更好的應用程式。

執行效果

請注意,當您完成滾動表檢視時,可見行上的影象將立即開始處理。

其他

本文首發於RiverLi的個人公眾號,轉載請註明出處。
歡迎關注我公眾號與我交流。

RiverLi的公眾號

相關文章