swift语言之多线程操作和操作队列(下)———坚持51天吃掉大象(写技术文章)
欢迎有兴趣的朋友,参与我的美女同事发起的活动《51天吃掉大象》,该美女真的很疯狂,希望和大家一起坚持51天做一件事情,我加入这个队伍,希望坚持51天每天写一篇技术文章。关注她的微信公众号:zhangmanyuheart了解更多吧。
继续上篇的文章《swift语言之多线程操作和操作队列(上)———坚持51天吃掉大象(写技术文章)》
优化我们的程序
目前程序未使用多线程,如果我们仔细分析,会发现有三个耗时的地方,现在我们需要把他们放到其他线程上去,这样主线程就有足够的空间和时间来响应用户操作。

根据分析我们可以得知,我们需要一个线程专门响应用户操作,一个线程处理下载数据源和图片,还要一个线程执行添加滤镜操作。
我们可以大概的这么去重新构造我的程序设计。我们可以先呈现一个空表格,然后当数据源下载成功后我们刷新表格,刷新表格属于用户操作界面应该放在主线程上。根据数据源内容我们可以知道图片的下载地址,我们最好不要一次性加载所有的图片,这样显然比较耗时,我们只需要知道表中哪些行是用户可以看得到的,然后加载对应行的的图片数据即可,当图片下载完成,程序再呈现图片,再在另外一个线程给图片添加滤镜。这样就完美解决了问题。
解决思路可以参看下图表:

我现在只需要重点关注图片处于什么状态,是正在下载还是现在完成,又或者滤镜是否添加?然后给图片添加不同的操作,并且希望用户下拉时,可以取消看不见的表格的相应操作,并开始或恢复用户可见范围的相应操作。因此在这种情况下适合使用NSOperation,而不是GCD。
让我们写代码吧!
首先新建一个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:NSURL
var state = PhotoRecordState.New
var image = UIImage(named: "Placeholder")
init(name:String, url:NSURL) {
self.name = name
self.url = url
}
}
这个类用来实现程序的图片展示,并且包含图片所处的状态,默认为.New,代表是新建状态,并有一个默认占位图片。
为了了解图片操作的每个状态,我们需要再创建一个类,名称为 PhotoOperations.swift。添加代码:
class PendingOperations {
lazy var downloadsInProgress = [NSIndexPath:NSOperation]()
lazy var downloadQueue:NSOperationQueue = {
var queue = NSOperationQueue()
queue.name = "Download queue"
queue.maxConcurrentOperationCount =
return queue
}()
lazy var filtrationsInProgress = [NSIndexPath:NSOperation]()
lazy var filtrationQueue:NSOperationQueue = {
var queue = NSOperationQueue()
queue.name = "Image Filtration queue"
queue.maxConcurrentOperationCount =
return queue
}()
}
这个类创建了两个字典,用于记录表格的下载和添加滤镜的操作,以及每个操作的队列。
如你看到的那样,创建队列非常简单。为了调试能查看到队列,最好给队列命名。代码将queue.maxConcurrentOperationCount命名为1,是为让你更直观的看到操作是一个一个执行的。一般我们不需要设置此属性,而交给系统自己决定。系统会根据硬件状态,已经资源占用情况,然后决定给程序多少个线程。
现在添加下载和添加滤镜操作,添加如下代码:
class ImageDownloader: NSOperation {
//图片类对象
let photoRecord: PhotoRecord
//2初始化
init(photoRecord: PhotoRecord) {
self.photoRecord = photoRecord
}
//3重写main方法,执行任务的方法
override func main() {
//4如果取消操作则不执行
if self.cancelled {
return
}
//5下载图片数据
let imageData = NSData(contentsOfURL:self.photoRecord.url)
//6再次检查是否取消操作
if self.cancelled {
return
}
//7如果获取到了数据,就添加到图片记录中,并将记录标记为.Downloaded,如果没有图片数据就标记为.Failed
if imageData?.length > {
self.photoRecord.image = UIImage(data:imageData!)
self.photoRecord.state = .Downloaded
}
else
{
self.photoRecord.state = .Failed
self.photoRecord.image = UIImage(named: "Failed")
}
}
}
NSOperation是一个抽象类,需要继承才能使用,每个子类代表一个具体的任务。
我们继续创建另外一个操作
class ImageFiltration: NSOperation {
let photoRecord: PhotoRecord
init(photoRecord: PhotoRecord) {
self.photoRecord = photoRecord
}
override func main () {
if self.cancelled {
return
}
if self.photoRecord.state != .Downloaded {
return
}
if let filteredImage = self.applySepiaFilter(self.photoRecord.image!) {
self.photoRecord.image = filteredImage
self.photoRecord.state = .Filtered
}
}
}
给ImageFiltration类添加一个应用滤镜的方法:
func applySepiaFilter(image:UIImage) -> UIImage? {
let inputImage = CIImage(data:UIImagePNGRepresentation(image))
if self.cancelled {
return nil
}
let context = CIContext(options:nil)
let filter = CIFilter(name:"CISepiaTone")
filter.setValue(inputImage, forKey: kCIInputImageKey)
filter.setValue(0.8, forKey: "inputIntensity")
let outputImage = filter.outputImage
if self.cancelled {
return nil
}
let outImage = context.createCGImage(outputImage, fromRect: outputImage.extent())
let returnImage = UIImage(CGImage: outImage)
return returnImage
}
这个方法和在 ListViewController一样,放在这里,就是把把它添加到操作里,方便调用。
到此我们创建好工具类了,现在我们开始修改ListViewController.swift。删除lazy var photos属性声明,取而代之添加如下代码:
//保存图片信息数组
var photos = [PhotoRecord]()
//管理状态操作
let pendingOperations = PendingOperations()
给该类添加一个方法:
func fetchPhotoDetails() {
let request = NSURLRequest(URL:dataSourceURL!)
UIApplication.sharedApplication().networkActivityIndicatorVisible = true
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue()) {response,data,error in
if data != nil {
do {
let datasourceDictionary = try NSPropertyListSerialization.propertyListWithData(data!, options: NSPropertyListMutabilityOptions.Immutable, format: nil) as! NSDictionary
for(key,value) in datasourceDictionary {
let name = key as? String
let url = NSURL(string:value as? String ?? "")
if name != nil && url != nil {
let photoRecord = PhotoRecord(name:name!, url:url!)
self.photos.append(photoRecord)
}
}
self.tableView.reloadData()
} catch{
print(error)
}
}
if error != nil {
let alert = UIAlertView(title:"Oops!",message:error!.localizedDescription, delegate:nil, cancelButtonTitle:"OK")
alert.show()
}
UIApplication.sharedApplication().networkActivityIndicatorVisible = false
}
}
在viewDidLoad方法中调用这个方法。
fetchPhotoDetails()
修改 tableView(_:cellForRowAtIndexPath:)的内容,改成如下代码:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("CellIdentifier", forIndexPath: indexPath) as! UITableViewCell
//
if cell.accessoryView == nil {
let indicator = UIActivityIndicatorView(activityIndicatorStyle: .Gray)
cell.accessoryView = indicator
}
let indicator = cell.accessoryView as! UIActivityIndicatorView
//
let photoDetails = photos[indexPath.row]
//
cell.textLabel?.text = photoDetails.name
cell.imageView?.image = photoDetails.image
//
switch (photoDetails.state){
case .Filtered:
indicator.stopAnimating()
case .Failed:
indicator.stopAnimating()
cell.textLabel?.text = "Failed to load"
case .New, .Downloaded:
indicator.startAnimating()
self.startOperationsForPhotoRecord(photoDetails,indexPath:indexPath)
}
return cell
}
移除applySepiaFilter方法,替换如下方法:
func startOperationsForPhotoRecord(photoDetails: PhotoRecord, indexPath: NSIndexPath){
switch (photoDetails.state) {
case .New:
startDownloadForRecord(photoDetails, indexPath: indexPath)
case .Downloaded:
startFiltrationForRecord(photoDetails, indexPath: indexPath)
default:
NSLog("do nothing")
}
}
继续添加如下方法:
fun startDownloadForRecord(photoDetails: PhotoRecord, indexPath: NSIndexPath){
//
if let downloadOperation = pendingOperations.downloadsInProgress[indexPath] {
return
}
//
let downloader = ImageDownloader(photoRecord: photoDetails)
//
downloader.completionBlock = {
if downloader.cancelled {
return
}
dispatch_async(dispatch_get_main_queue(), {
self.pendingOperations.downloadsInProgress.removeValueForKey(indexPath)
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
})
}
//
pendingOperations.downloadsInProgress[indexPath] = downloader
//
pendingOperations.downloadQueue.addOperation(downloader)
}
func startFiltrationForRecord(photoDetails: PhotoRecord, indexPath: NSIndexPath){
if let filterOperation = pendingOperations.filtrationsInProgress[indexPath]{
return
}
let filterer = ImageFiltration(photoRecord: photoDetails)
filterer.completionBlock = {
if filterer.cancelled {
return
}
dispatch_async(dispatch_get_main_queue(), {
self.pendingOperations.filtrationsInProgress.removeValueForKey(indexPath)
self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
})
}
pendingOperations.filtrationsInProgress[indexPath] = filterer
pendingOperations.filtrationQueue.addOperation(filterer)
}
我们的重构基本完成,我们运行下看看,会看到如下效果图:

注意到了木有,奇迹发生了,图片只在可见的时候才会加载和添加滤镜。并且不会再卡了有木有。
继续调优
如果你向下滑动表格,那些从屏幕消失的图片仍在下载或添加滤镜,如果滑动快速的话,程序就会忙着加载图片和添加滤镜了,并占用贷款,影响看见cell的下载了。因此最理想的状态,就是当Cell行消失时,我们停止下载,从而优先下载可见的cell。
回到Xcode,修改ListViewController.swift文件,然后找到tableView(_:cellForRowAtIndexPath:)方法,给 self.startOperationsForPhotoRecord(photoDetails, indexPath: indexPath)添加判断:
if (!tableView.dragging && !tableView.decelerating) {
self.startOperationsForPhotoRecord(photoDetails, indexPath: indexPath)
}
再继续添加如下内容:
override func scrollViewWillBeginDragging(scrollView: UIScrollView) {
//
suspendAllOperations()
}
override func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
//
if !decelerate {
loadImagesForOnscreenCells()
resumeAllOperations()
}
}
override func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
//
loadImagesForOnscreenCells()
resumeAllOperations()
}
func suspendAllOperations () {
pendingOperations.downloadQueue.suspended = true
pendingOperations.filtrationQueue.suspended = true
}
func resumeAllOperations () {
pendingOperations.downloadQueue.suspended = false
pendingOperations.filtrationQueue.suspended = false
}
func loadImagesForOnscreenCells () {
//
if let pathsArray = tableView.indexPathsForVisibleRows() {
//
var allPendingOperations = Set(pendingOperations.downloadsInProgress.keys.array)
allPendingOperations.unionInPlace(pendingOperations.filtrationsInProgress.keys.array)
//
var toBeCancelled = allPendingOperations
let visiblePaths = Set(pathsArray as! [NSIndexPath])
toBeCancelled.subtractInPlace(visiblePaths)
//
var toBeStarted = visiblePaths
toBeStarted.subtractInPlace(allPendingOperations)
//
for indexPath in toBeCancelled {
if let pendingDownload = pendingOperations.downloadsInProgress[indexPath] {
pendingDownload.cancel()
}
pendingOperations.downloadsInProgress.removeValueForKey(indexPath)
if let pendingFiltration = pendingOperations.filtrationsInProgress[indexPath] {
pendingFiltration.cancel()
}
pendingOperations.filtrationsInProgress.removeValueForKey(indexPath)
}
//
for indexPath in toBeStarted {
let indexPath = indexPath as NSIndexPath
let recordToProcess = self.photos[indexPath.row]
startOperationsForPhotoRecord(recordToProcess, indexPath: indexPath)
}
}
}
这已经是最后一步了,恭喜你,也辛苦你了,不过这是值得的,现在你运行看看,一个响应用户及时,并且资源管理合理的程序就在你手上诞生了。注意一下当你滚动表格结束,可见的表格行将马上开始处理操作。

swift语言之多线程操作和操作队列(下)———坚持51天吃掉大象(写技术文章)的更多相关文章
- swift语言之多线程操作和操作队列(上)———坚持51天吃掉大象
欢迎有兴趣的朋友,参与我的美女同事发起的活动<51天吃掉大象>,该美女真的很疯狂,希望和大家一起坚持51天做一件事情,我加入这个队伍,希望坚持51天每天写一篇技术文章.关注她的微信公众号: ...
- 基于Swift语言开发微信、QQ和微博的SSO授权登录代码分析
前言 Swift 语言,怎么说呢,有一种先接受后排斥.又欢迎的感觉,纵观国外大牛开源框架或项目演示,Swift差点儿占领了多半,而国内尽管出现非常多相关技术介绍和教程,可是在真正项目开发中使用的占领非 ...
- HDFS简单介绍及用C语言訪问HDFS接口操作实践
一.概述 近年来,大数据技术如火如荼,怎样存储海量数据也成了当今的热点和难点问题,而HDFS分布式文件系统作为Hadoop项目的分布式存储基础,也为HBASE提供数据持久化功能,它在大数据项目中有很广 ...
- YTU 2974: C语言习题5.26--文件操作3
2974: C语言习题5.26--文件操作3 时间限制: 1 Sec 内存限制: 128 MB 提交: 213 解决: 92 题目描述 文本文件score.dic 中存储了n名学生的信息(班级编号 ...
- YTU 2973: C语言习题5.25--文件操作2
2973: C语言习题5.25--文件操作2 时间限制: 1 Sec 内存限制: 128 MB 提交: 242 解决: 105 题目描述 文本文件score.dic 中存储了n名学生的信息(班级编 ...
- YTU 2972: C语言习题5.24--文件操作1
2972: C语言习题5.24--文件操作1 时间限制: 1 Sec 内存限制: 128 MB 提交: 248 解决: 94 题目描述 文本文件score.dic 中存储了n名学生的信息(班级编号 ...
- Qt 多线程与数据库操作需要注意的几点问题(QSqlDatabase对象只能在当前线程里使用)
彻底抛弃MFC, 全面应用Qt 已经不少时间了.除了自己看书按步就班做了十几个验证性的应用,还正式做了3个比较大的行业应用,总体感觉很好.Native C++ 下, Qt 基本是我用过的最简便的界面库 ...
- Qt 多线程与数据库操作需要注意的几点问题
源地址:http://blog.csdn.net/goldenhawking/article/details/10811409 彻底抛弃MFC, 全面应用Qt 已经不少时间了.除了自己看书按步就班做了 ...
- C语言对mysql数据库的操作
原文:C语言对mysql数据库的操作 这已经是一相当老的话题.不过今天我才首次使用,把今天的一些体会写下来,也许能给一些新手带来一定的帮助,更重要的是供自己今后忘记的怎么使用而进行查阅的! 我们言归正 ...
随机推荐
- 黑盒测试在App自动化测试中的应用
黑盒测试在App自动化测试中的应用 不废话,直接来. 先说说什么是黑盒测试 黑盒测试,这里就说的是app功能测试,之前看到一个介绍说,就是在测试中,把测试对象看作一个黑盒子.利用黑盒测试法进行动态测试 ...
- drush cc all 报错
请看好 指明了Module文件的行数 报错一定要多看看哦.
- C/C++中float和double的存储结构
int main (int argc, char **argv) { float a = 1.0f; cout <<"(int&)a = "<<(i ...
- IIS提示Server Application Unavailable
浏览器访问网站,IIS提示Server Application Unavailable,我的解决方式是进入IIS管理界面,找到对应的站点,之后重启这个站点.
- jQuery.outerWidth() 函数详解
outerWidth()函数用于设置或返回当前匹配元素的外宽度. 外宽度默认包括元素的内边距(padding).边框(border),但不包括外边距(margin)部分的宽度.你也可以指定参数为tru ...
- CentOS6安装python2.7
第一次用centOS,感觉好高大上,安装了差不多一个半小时,学习了挺多命令的 1. 检查centOS中默认的python版本,一般是python2.6. 命令:python –v 2. 安装GCC ...
- 130. Surrounded Regions -- 被某字符包围的区域
Given a 2D board containing 'X' and 'O', capture all regions surrounded by 'X'. A region is captured ...
- C#运算除法和求整
在C#与法中,“/”除后所得的值的类型,跟他的除数和被除数的类型有关.如: int a=4; int b=5 ...
- 使用git上传项目
1. 安装Git 2. 安装TortoiseGit 3.任意文件夹选择「TortoiseGit」>「settings」,打开如下界面. 3. 生成SSH公钥 3.1运行Git Bash,如下命令 ...
- Go语言并发与并行学习笔记(一)
转:http://blog.csdn.net/kjfcpua/article/details/18265441 如果不是我对真正并行的线程的追求,就不会认识到Go有多么的迷人. Go语言从语言层面上就 ...