Operation和OperationQueue实战:异步下载图片并给图片加滤镜
系统: 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
这里有两个工程,
- Starter 实现是卡住主线程,等图片下载完毕并加上滤镜后才能显示。
运行后查看CPU使用情况,发现基本上都在Thread1也就是主线程一直在处理。
- Finished 实现了异步下载,下载完毕后加上滤镜,如果滑动,则停止所有下载,移除没有在当前界面的下载队列,只下载当前页面的图片下载。
改进后看CPU使用情况,多个线程Thread会在处理工作
改造为异步下载和加滤镜流程图
- 开始的时候显示空白的tableView,显示indicator表示在下载。
- 异步线程下载在当前页面的图片,下载完毕后切回主线程,reload对应tableView indexPath
- 接着异步线程给下载完的图片加上滤镜,加好滤镜后切回主线程,reload对应tableView indexPath.
- 如果滑动,则停止所有的下载,移除已经不再当前页面的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}()
}
注释:
- 创建正在下载图片线程的容器
downloadsInProgress: [IndexPath: Operation]
,加滤镜线程的容器filtrationsInProgress: [IndexPath: Operation]
。 - 创建下载图片队列
downloadQueue: OperationQueue
, 加载滤镜队列filtrationQueue: OperationQueue
。 - 所有的属性都是懒加载用
lazy
修饰,表示属性第一次调用后才会加载。提高应用打开速度。 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
来实现. 每个子类实现具体的任务,可以有多个任务。如图:
上面标号的注释:
- 加一个常量
photoRecord: PhotoRecord
. - 初始化的时候赋值给
photoRecord
. main()
是复写了父类的Operation
方法,线程启动后实际执行的任务.- 检查任务状态是否取消了,取消则退出.
- 下载图片二进制内容.
- 再次检查任务状态是否取消了,取消则退出.
- 如果有图片二进制数据,则给图片赋值,并把状态更新为下载完毕。否则,给图片显示失败图片,并把状态更新为失败.
图片加滤镜线程
在文件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
的主线程上执行,现在放到滤镜线程里面执行。
- 创建图片输入源
inputImage: CIImage
- 创建上下文对象
let context = CIContext(options: nil)
- 创建深褐色滤镜对象
filter = CIFilter(name: "CISepiaTone")
,并设置图片输入源属性,头密度属性 - 输出图片
filter.outputImage
,通过context
生成结果图片outImage
- 基于
outImage
创建UIImage
对象,并返回UIImage对象
修改ListViewController.swift
为后台进程处理图片
1.在类ListViewController
中删除属性lazy var photos
,最上面添加如下属性
var photos: [PhotoRecord] = []
let pendingOperations = PendingOperations()
注释:
photos
存储从ClassicPhotosDictionary.plist
获取的图片对象,回来的时候又name
,url
属性。笔者把改文件也放到本地,可以看下图。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()
}
注释:
- 创建
URLSession
的任务dataTask
,在后台线程下载图片列表信息. - 创建
UIAlertController
处理错误逻辑. - 如果请求数据成功,根据
property list
创建dictionary
.dictionary
的key
表示图片名字,value
表示图片URL
. - 通过
dictionary
创建PhotoRecord
对象数组. - 切回主线程重新加载tableView,这个时候tableView都是空图片的Cell.
- 如果发生错误,则在主线程弹出
UIAlertController
. - 开始下载任务.
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
}
注释:
- 创建
UIActivityIndicatorView
,并设置为cell
的属性accessoryView
,下载图片的时候加载动画. - 从数据源
photos
获取当前的PhotoRecord
对象. - 设置
cell’s
的文字和图片. - 检查record的状态,根据状态设置
indicator
指示器是否显示动画,更新文字信息,并启动图片下载和图片滤镜线程.
5. 增加方法startOperations
在cellForRowAt
方法下面
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)
}
注释:下载图片线程和滤镜处理线程的流程都是一样的,所以只讲下载图片线程的流程。
- 首先,检查
indexPath
去看是否已经有线程operation在下载非重复数组downloadsInProgress
中. 如果有, 忽略掉该请求.
如果没有,创建ImageDownloader
对象. - 增加一个完成回调
completion block
,当线程处理完毕后会调用. 敲黑板了:在线程取消cancelled
操作,也会调用completion block
, 所以必须要检查线程的状态,并相应处理. 如果下载成功,则在主线程刷新tableView
的那一行IndexPath
. - 添加线程
operation
到字典downloadsInProgress
,跟着正在下载的线程. - 添加线程
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()
}
注释:
- 当用户开始滑动的时候,需要马上挂起所有的线程(suspend all operations), 并看看用户滑动到哪里停止. 方法
suspendAllOperations
马上就会实现. - 如果不是降速(decelerate is false), 表示用户停止滑动table view. 因此需要恢复挂起的线程, 取消已经离开屏幕cells的线程, 并启动正在屏幕cells的线程. 方法
loadImagesForOnscreenCells
和resumeAllOperations
一样马上就会实现. - 这个代理方法告诉你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()
这个方法有点复杂. 这里做了如下事情:
- 创建一个数组包含所有可见cells的indexPaths.
- 构造
set<IndexPath>
包含所有处理中线程:包括下载图片线程,以及滤镜处理线程. - 构造
set<IndexPath>
包含所有已经离开屏幕的线程. 开始的时候包括所有的线程,然后移除可见cells的线程,剩下的就是离开屏幕的线程. - 构造
set<IndexPath>
包含所有在当前. 开始的时候包括所有可见cells的线程, 接着移除已经在处理中的线程. - 遍历所有被取消的线程, 并从
PendingOperations
移除已经取消的对象. - 遍历所有启动的线程,调用方法
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实战:异步下载图片并给图片加滤镜相关推荐
- Python爬虫实战批量下载高清美女图片,男生最爱的案例吧!
彼岸图网站里有大量的高清图片素材和壁纸,并且可以免费下载,读者也可以根据自己需要爬取其他类型图片,方法是类似的,本文通过python爬虫批量下载网站里的高清美女图片,熟悉python写爬虫的基本方法: ...
- Swift多线程:使用GCD实现异步下载图片
GCD属于系统及的线程管理,功能很强大,比上两次咱们分享的Operation要强大.有很多老前辈们已经创造了非常非常多的资料介绍GCD,因为大家都是把GCD放在了多线程内容分享的最开始,所以导致好多好 ...
- [Swift]在不依赖三方库的情况下如何异步下载和缓存图片?
在可滚动视图(如UITableView)中异步加载大量图片是一个很常见的任务. 然而,在图片正在下载的同时又要保持应用程序流畅滚动,可能有点挑战. 许多开发人员依靠像Alamofire和SDWebIm ...
- [翻译] LASIImageView - 显示进度指示并异步下载图片
LASIImageView – download image with progress indicator 翻译原网址:http://lukagabric.com/lasiimageview-d ...
- Scrapy图片分类异步下载
可在pipeline中自定义一个图片类来继承Scrapy的图片类(ImagesPipeline[可以实现异步下载]),并重写ImagesPipeline的方法,来实现图片的异步下载 Scrapy的Im ...
- python 协程之异步下载图片
1.安装第三方模块 pip install aiofiles pip install aiohttp 2.示例 #! /usr/bin/env python3 import asyncio impor ...
- 使用开源库 SDWebImage 异步下载缓存图片(持续更新)
source https://github.com/rs/SDWebImage APIdoc http://hackemist.com/SDWebImage/doc Asynchronous im ...
- python爬虫实战——自动下载百度图片(文末附源码)
用Python制作一个下载图片神器 前言 这个想法是怎么来的? 很简单,就是不想一张一张的下载图片,嫌太慢. 在很久很久以前,我比较喜欢收集各种动漫的壁纸,作为一个漫迷,自然是能收集多少就收集多少.小 ...
- CGD 异步下载图片
CGD 异步下载图片 dispatch_queue_t squeue = dispatch_queue_create ( "abc" , NULL ); di ...
- python 异步下载图片_python3抓取异步百度瀑布流动态图片(二)get、json下载代码讲解...
制作解析网址的get 1 defgethtml(url,postdata):2 3 header = {'User-Agent':4 'Mozilla/5.0 (Windows NT 10.0; WO ...
最新文章
- php缩图代码是什么,php生成缩略图示例代码分享(使用gd库实现)
- 10款著名的代码(文本)编辑器
- Spring_mvc ioc/DI 控制反转与依赖注入
- 第七章 递推与递归 第3课 攀天梯(ladder) --《聪明人的游戏:信息学探秘.提高篇》
- 华为linux配置ip地址命令是什么,华为S5700基础配置命令
- 数学建模(5)---煤矸石堆积问题
- python mobilenetssd android_MobileNetV2-SSDLite运行
- ubuntu下opencv3和opencv2共存
- 机器学习基石01:机器学习简介
- [整理]VS2010中文版配置opencv2.4.8
- 软件服务器 配置文件,服务器生成软件配置文件
- windows 下 redis服务经常自动关闭
- Linux内核中的IPSEC实现(1)
- 教你用Python拨打电话
- JQuery自定义属性的设置和获取
- C#获取系统空闲时间
- 帕斯卡命名法、驼峰命名法、下划线命名法
- 企业级SSD产品对比
- 迅投qmt量化交易系统以及实盘介绍
- 苹果iPad大陆用户首评:速度流畅 屏幕优秀(组图)
热门文章
- python人脸识别程序如何嵌入到app_开源|手把手教你用Python进行人脸识别(附源代码)...
- mysql怎么显示创表的语句_MySql轻松入门系列——第二站 使用visual studio 对mysql进行源码级调试...
- Java 后端MD5加密
- Windows C盘清理指北
- APK反编译之二:工具介绍
- shell循环和函数引用
- D3D 扎带 小样本
- python文件中写中文_解决python中csv文件中文写入问题
- Ubuntu之hadoop非分布式(单机)和伪分布式安装
- error: 'EOF' was not declared in this scope的解决办法