系统: Mac OS 10.15.1, XCode 11,swift 5.0
写作时间:2019-11-20

说明

跟UI相关的操作需要在主线程处理(参考:为什么必须在主线程操作UI),下载、处理数据等可以放到其它线程异步处理。

本文为翻译文章:Operation and OperationQueue Tutorial in Swift。笔者更进一步的地方在于用Swift 5.0重写了例子。例子过程图片如下:最开始卡主主线程 > 改写为在异步进程下载 > 滑动时停止下载、只下载当前显示的图片

这里会用到Operation的相关知识,如果想了解细节请参考:Operation和OperationQueue详解

工程代码下载

https://github.com/zgpeace/OperationQueuePhotos
这里有两个工程,

  1. Starter 实现是卡住主线程,等图片下载完毕并加上滤镜后才能显示。

    运行后查看CPU使用情况,发现基本上都在Thread1也就是主线程一直在处理。
  2. Finished 实现了异步下载,下载完毕后加上滤镜,如果滑动,则停止所有下载,移除没有在当前界面的下载队列,只下载当前页面的图片下载。

    改进后看CPU使用情况,多个线程Thread会在处理工作

改造为异步下载和加滤镜流程图

  1. 开始的时候显示空白的tableView,显示indicator表示在下载。
  2. 异步线程下载在当前页面的图片,下载完毕后切回主线程,reload对应tableView indexPath
  3. 接着异步线程给下载完的图片加上滤镜,加好滤镜后切回主线程,reload对应tableView indexPath.
  4. 如果滑动,则停止所有的下载,移除已经不再当前页面的indexPath 下载对象。停止滑动后,添加在当前页面的下载对象,开始下载和加滤镜。

创建图片处理状态,以及图片类

新建类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: Stringlet url: URLvar state = PhotoRecordState.newvar image = UIImage(named: "Placeholder")init(name:String, url:URL) {self.name = nameself.url = url}
}

注释:图片处理类PhotoRecord刚开始的状态是PhotoRecordState.new, 默认图片显示占位图var image = UIImage(named: "Placeholder").

下载线程的管理容器

在文件PhotoOperations.swift的最下面,添加如下代码

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

注释:

  1. 创建正在下载图片线程的容器downloadsInProgress: [IndexPath: Operation],加滤镜线程的容器filtrationsInProgress: [IndexPath: Operation]
  2. 创建下载图片队列downloadQueue: OperationQueue, 加载滤镜队列filtrationQueue: OperationQueue
  3. 所有的属性都是懒加载用lazy修饰,表示属性第一次调用后才会加载。提高应用打开速度。
  4. queue.maxConcurrentOperationCount = 1线程每次只处理一个,这样子可以看到图片是挨个处理的。一般的情况下设置6~8个(看手机性能),都可以。

图片下载线程

在文件PhotoOperations.swift的最下面,添加如下代码

class ImageDownloader: Operation {//1let photoRecord: PhotoRecord//2init(_ photoRecord: PhotoRecord) {self.photoRecord = photoRecord}//3override func main() {//4if isCancelled {return}//5guard let imageData = try? Data(contentsOf: photoRecord.url) else { return }//6if isCancelled {return}//7if !imageData.isEmpty {photoRecord.image = UIImage(data: imageData)photoRecord.state = .downloaded} else {photoRecord.state = .failedphotoRecord.image = UIImage(named: "Failed")}}
}

注释:Operation是抽象类, 需要子类ImageDownloader来实现. 每个子类实现具体的任务,可以有多个任务。如图:

上面标号的注释:

  1. 加一个常量photoRecord: PhotoRecord.
  2. 初始化的时候赋值给photoRecord.
  3. main() 是复写了父类的Operation方法,线程启动后实际执行的任务.
  4. 检查任务状态是否取消了,取消则退出.
  5. 下载图片二进制内容.
  6. 再次检查任务状态是否取消了,取消则退出.
  7. 如果有图片二进制数据,则给图片赋值,并把状态更新为下载完毕。否则,给图片显示失败图片,并把状态更新为失败.

图片加滤镜线程

在文件PhotoOperations.swift的最下面,添加如下代码

class ImageFiltration: Operation {let photoRecord: PhotoRecordinit(_ 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 = filteredImagephotoRecord.state = .filtered}}}

加滤镜线程ImageFiltration跟图片下载线程ImageDownloader的注释类似。

加滤镜的具体实现方法,在类ImageFiltration的最下面加上如下方法

func applySepiaFilter(_ image: UIImage) -> UIImage? {guard let data = image.pngData() 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的主线程上执行,现在放到滤镜线程里面执行。

  1. 创建图片输入源inputImage: CIImage
  2. 创建上下文对象let context = CIContext(options: nil)
  3. 创建深褐色滤镜对象filter = CIFilter(name: "CISepiaTone"),并设置图片输入源属性,头密度属性
  4. 输出图片filter.outputImage,通过context生成结果图片outImage
  5. 基于 outImage创建UIImage对象,并返回UIImage对象

修改ListViewController.swift为后台进程处理图片

1.在类ListViewController 中删除属性lazy var photos,最上面添加如下属性

var photos: [PhotoRecord] = []
let pendingOperations = PendingOperations()

注释:

  1. photos 存储从ClassicPhotosDictionary.plist获取的图片对象,回来的时候又nameurl属性。笔者把改文件也放到本地,可以看下图。
  2. pendingOperations 存储下载图片线程容器和滤镜处理图片线程容器

2.在类ListViewController 中添加下载图片链接列表方法

// fetch photos
func fetchPhotosDetails() {let request = URLRequest(url: dataSourceURL)UIApplication.shared.isNetworkActivityIndicatorVisible = true// 1let task = URLSession(configuration: .default).dataTask(with: request) {data, response, error in// 2let alertController = UIAlertController(title: "Oops!",message: "There was an error fetching photo details",preferredStyle: .alert)let okAction = UIAlertAction(title: "OK",style: .default,handler: nil)alertController.addAction(okAction)if let data = data {do {// 3let datasourceDictionary = try PropertyListSerialization.propertyList(from: data,options: [], format: nil) as! [String: String]// 4for (name, value) in datasourceDictionary {let url = URL(string: value)if let url = url {let photoRecord = PhotoRecord(name: name, url: url)self.photos.append(photoRecord)}}// 5DispatchQueue.main.async {UIApplication.shared.isNetworkActivityIndicatorVisible = falseself.tableView.reloadData()}// 6} catch {DispatchQueue.main.async {self.present(alertController, animated: true, completion: nil)}}}// 6if error != nil {DispatchQueue.main.async {UIApplication.shared.isNetworkActivityIndicatorVisible = falseself.present(alertController, animated: true, completion: nil)}}}// 7task.resume()
}

注释:

  1. 创建URLSession的任务dataTask,在后台线程下载图片列表信息.
  2. 创建UIAlertController处理错误逻辑.
  3. 如果请求数据成功,根据property list创建dictionary. dictionarykey表示图片名字,value表示图片URL.
  4. 通过dictionary创建PhotoRecord对象数组.
  5. 切回主线程重新加载tableView,这个时候tableView都是空图片的Cell.
  6. 如果发生错误,则在主线程弹出UIAlertController.
  7. 开始下载任务.

3. 在viewDidLoad()的最下面增加下载代码

fetchPhotoDetails()

4. 替换方法tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell的实现逻辑

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {let cell = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier", for: indexPath) //1if cell.accessoryView == nil {let indicator = UIActivityIndicatorView(style: .gray)cell.accessoryView = indicator}let indicator = cell.accessoryView as! UIActivityIndicatorView//2let photoDetails = photos[indexPath.row]//3cell.textLabel?.text = photoDetails.namecell.imageView?.image = photoDetails.image//4switch (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,并设置为cell的属性accessoryView,下载图片的时候加载动画.
  2. 从数据源photos获取当前的PhotoRecord对象.
  3. 设置cell’s的文字和图片.
  4. 检查record的状态,根据状态设置indicator指示器是否显示动画,更新文字信息,并启动图片下载和图片滤镜线程.

5. 增加方法startOperationscellForRowAt方法下面

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")}
}

这里图片下载线程和图片路径处理线程为两个独立的线程,这样的好处是,用户滑动列表,停止所有的线程操作,在有滑动列表到已经下载好的图片的位置,则直接启动滤镜处理线程。完美吧?

6. 增加线程启动方法(下载图片线程和滤镜处理线程)

func startDownload(for photoRecord: PhotoRecord, at indexPath: IndexPath) {//1guard pendingOperations.downloadsInProgress[indexPath] == nil else {return}//2let downloader = ImageDownloader(photoRecord)//3downloader.completionBlock = {if downloader.isCancelled {return}DispatchQueue.main.async {self.pendingOperations.downloadsInProgress.removeValue(forKey: indexPath)self.tableView.reloadRows(at: [indexPath], with: .fade)}}//4pendingOperations.downloadsInProgress[indexPath] = downloader//5pendingOperations.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] = filtererpendingOperations.filtrationQueue.addOperation(filterer)
}

注释:下载图片线程和滤镜处理线程的流程都是一样的,所以只讲下载图片线程的流程。

  1. 首先,检查indexPath去看是否已经有线程operation在下载非重复数组 downloadsInProgress中. 如果有, 忽略掉该请求.
    如果没有,创建ImageDownloader对象.
  2. 增加一个完成回调completion block,当线程处理完毕后会调用. 敲黑板了:在线程取消cancelled操作,也会调用completion block, 所以必须要检查线程的状态,并相应处理. 如果下载成功,则在主线程刷新tableView的那一行IndexPath.
  3. 添加线程operation到字典downloadsInProgress,跟着正在下载的线程.
  4. 添加线程operation到下载队列中download queue. 这个操作会自动触发启动线程处理,调用方法main().

运行代码

可以跑起来了,如果一启动就滑动scrollView,会看到图片等一会才会加载。说明图片下载和图片滤镜处理是从上往下处理的。效果图如下:

改进:离开的Cell停止图片下载和滤镜处理

当滑动tableView的时候,那些已经离开屏幕的cells还在处理下载图片和滤镜处理. 如果快速滑动,App将会忙于下载和滤镜那些已经远离的cells,用户并看不到. 理想的情况,App需要取消下载和滤镜那些已经离开屏幕的cells,并优先处理目前显示的cells.

打开 ListViewController.swift,找到代理方法tableView(_:cellForRowAtIndexPath:), 把代码startOperationsForPhotoRecord包在条件语句下 :

if !tableView.isDragging && !tableView.isDecelerating {startOperations(for: photoDetails, at: indexPath)
}

滑动取消所有线程操作,实现scrollView的delegate即可(UITableView是继承UIScrollView的)

// MARK: - scrollView delegate
override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {//1suspendAllOperations()
}override func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {//2if !decelerate {loadImagesForOnscreenCells()resumeAllOperations()}
}override func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {//3loadImagesForOnscreenCells()resumeAllOperations()
}

注释:

  1. 当用户开始滑动的时候,需要马上挂起所有的线程(suspend all operations), 并看看用户滑动到哪里停止. 方法suspendAllOperations 马上就会实现.
  2. 如果不是降速(decelerate is false), 表示用户停止滑动table view. 因此需要恢复挂起的线程, 取消已经离开屏幕cells的线程, 并启动正在屏幕cells的线程. 方法loadImagesForOnscreenCellsresumeAllOperations 一样马上就会实现.
  3. 这个代理方法告诉你table view停止滚动, 所以你需要跟#2 一样处理.

在类ListViewController.swift添加如下方法

func suspendAllOperations() {pendingOperations.downloadQueue.isSuspended = truependingOperations.filtrationQueue.isSuspended = true
}func resumeAllOperations() {pendingOperations.downloadQueue.isSuspended = falsependingOperations.filtrationQueue.isSuspended = false
}func loadImagesForOnscreenCells() {//1if let pathsArray = tableView.indexPathsForVisibleRows {//2var allPendingOperations = Set(pendingOperations.downloadsInProgress.keys)allPendingOperations.formUnion(pendingOperations.filtrationsInProgress.keys)//3var toBeCancelled = allPendingOperationslet visiblePaths = Set(pathsArray)toBeCancelled.subtract(visiblePaths)//4var toBeStarted = visiblePathstoBeStarted.subtract(allPendingOperations)//5for 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)}//6for indexPath in toBeStarted {let recordToProcess = photos[indexPath.row]startOperations(for: recordToProcess, at: indexPath)}}
}

注释:
suspendAllOperations()resumeAllOperations() 比较直接明了:OperationQueues 可以通过设置属性suspended 为true表示挂起. 这将挂起队列queue中的所有线程 — 你不能挂起指定的单个线程.

loadImagesForOnscreenCells()这个方法有点复杂. 这里做了如下事情:

  1. 创建一个数组包含所有可见cells的indexPaths.
  2. 构造set<IndexPath>包含所有处理中线程:包括下载图片线程,以及滤镜处理线程.
  3. 构造set<IndexPath>包含所有已经离开屏幕的线程. 开始的时候包括所有的线程,然后移除可见cells的线程,剩下的就是离开屏幕的线程.
  4. 构造set<IndexPath>包含所有在当前. 开始的时候包括所有可见cells的线程, 接着移除已经在处理中的线程.
  5. 遍历所有被取消的线程, 并从PendingOperations 移除已经取消的对象.
  6. 遍历所有启动的线程,调用方法startOperations(for:at:) .

运行代码,讲得到优化后的效果

总结

本文使用Operation实现了异步下载图片,异步滤镜处理图片。并在滑动的时候挂起所有的线程,停止滑动后,移除已经离开屏幕cells的线程,启动在当前屏幕的线程。

这里没有讲到的是Operation的依赖dependency,如果想了解dependency知识请参考:Operation和OperationQueue详解

代码下载

https://github.com/zgpeace/OperationQueuePhotos

参考

https://www.raywenderlich.com/5293-operation-and-operationqueue-tutorial-in-swift

Operation和OperationQueue实战:异步下载图片并给图片加滤镜相关推荐

  1. Python爬虫实战批量下载高清美女图片,男生最爱的案例吧!

    彼岸图网站里有大量的高清图片素材和壁纸,并且可以免费下载,读者也可以根据自己需要爬取其他类型图片,方法是类似的,本文通过python爬虫批量下载网站里的高清美女图片,熟悉python写爬虫的基本方法: ...

  2. Swift多线程:使用GCD实现异步下载图片

    GCD属于系统及的线程管理,功能很强大,比上两次咱们分享的Operation要强大.有很多老前辈们已经创造了非常非常多的资料介绍GCD,因为大家都是把GCD放在了多线程内容分享的最开始,所以导致好多好 ...

  3. [Swift]在不依赖三方库的情况下如何异步下载和缓存图片?

    在可滚动视图(如UITableView)中异步加载大量图片是一个很常见的任务. 然而,在图片正在下载的同时又要保持应用程序流畅滚动,可能有点挑战. 许多开发人员依靠像Alamofire和SDWebIm ...

  4. [翻译] LASIImageView - 显示进度指示并异步下载图片

      LASIImageView – download image with progress indicator 翻译原网址:http://lukagabric.com/lasiimageview-d ...

  5. Scrapy图片分类异步下载

    可在pipeline中自定义一个图片类来继承Scrapy的图片类(ImagesPipeline[可以实现异步下载]),并重写ImagesPipeline的方法,来实现图片的异步下载 Scrapy的Im ...

  6. python 协程之异步下载图片

    1.安装第三方模块 pip install aiofiles pip install aiohttp 2.示例 #! /usr/bin/env python3 import asyncio impor ...

  7. 使用开源库 SDWebImage 异步下载缓存图片(持续更新)

    source  https://github.com/rs/SDWebImage APIdoc  http://hackemist.com/SDWebImage/doc Asynchronous im ...

  8. python爬虫实战——自动下载百度图片(文末附源码)

    用Python制作一个下载图片神器 前言 这个想法是怎么来的? 很简单,就是不想一张一张的下载图片,嫌太慢. 在很久很久以前,我比较喜欢收集各种动漫的壁纸,作为一个漫迷,自然是能收集多少就收集多少.小 ...

  9. CGD 异步下载图片

    CGD  异步下载图片 dispatch_queue_t   squeue =   dispatch_queue_create ( "abc" , NULL );       di ...

  10. python 异步下载图片_python3抓取异步百度瀑布流动态图片(二)get、json下载代码讲解...

    制作解析网址的get 1 defgethtml(url,postdata):2 3 header = {'User-Agent':4 'Mozilla/5.0 (Windows NT 10.0; WO ...

最新文章

  1. php缩图代码是什么,php生成缩略图示例代码分享(使用gd库实现)
  2. 10款著名的代码(文本)编辑器
  3. Spring_mvc ioc/DI 控制反转与依赖注入
  4. 第七章 递推与递归 第3课 攀天梯(ladder) --《聪明人的游戏:信息学探秘.提高篇》
  5. 华为linux配置ip地址命令是什么,华为S5700基础配置命令
  6. 数学建模(5)---煤矸石堆积问题
  7. python mobilenetssd android_MobileNetV2-SSDLite运行
  8. ubuntu下opencv3和opencv2共存
  9. 机器学习基石01:机器学习简介
  10. [整理]VS2010中文版配置opencv2.4.8
  11. 软件服务器 配置文件,服务器生成软件配置文件
  12. windows 下 redis服务经常自动关闭
  13. Linux内核中的IPSEC实现(1)
  14. 教你用Python拨打电话
  15. JQuery自定义属性的设置和获取
  16. C#获取系统空闲时间
  17. 帕斯卡命名法、驼峰命名法、下划线命名法
  18. 企业级SSD产品对比
  19. 迅投qmt量化交易系统以及实盘介绍
  20. 苹果iPad大陆用户首评:速度流畅 屏幕优秀(组图)

热门文章

  1. python人脸识别程序如何嵌入到app_开源|手把手教你用Python进行人脸识别(附源代码)...
  2. mysql怎么显示创表的语句_MySql轻松入门系列——第二站 使用visual studio 对mysql进行源码级调试...
  3. Java 后端MD5加密
  4. Windows C盘清理指北
  5. APK反编译之二:工具介绍
  6. shell循环和函数引用
  7. D3D 扎带 小样本
  8. python文件中写中文_解决python中csv文件中文写入问题
  9. Ubuntu之hadoop非分布式(单机)和伪分布式安装
  10. error: 'EOF' was not declared in this scope的解决办法