欢迎有兴趣的朋友,参与我的美女同事发起的活动《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天吃掉大象(写技术文章)的更多相关文章

  1. swift语言之多线程操作和操作队列(上)———坚持51天吃掉大象

    欢迎有兴趣的朋友,参与我的美女同事发起的活动<51天吃掉大象>,该美女真的很疯狂,希望和大家一起坚持51天做一件事情,我加入这个队伍,希望坚持51天每天写一篇技术文章.关注她的微信公众号: ...

  2. 基于Swift语言开发微信、QQ和微博的SSO授权登录代码分析

    前言 Swift 语言,怎么说呢,有一种先接受后排斥.又欢迎的感觉,纵观国外大牛开源框架或项目演示,Swift差点儿占领了多半,而国内尽管出现非常多相关技术介绍和教程,可是在真正项目开发中使用的占领非 ...

  3. HDFS简单介绍及用C语言訪问HDFS接口操作实践

    一.概述 近年来,大数据技术如火如荼,怎样存储海量数据也成了当今的热点和难点问题,而HDFS分布式文件系统作为Hadoop项目的分布式存储基础,也为HBASE提供数据持久化功能,它在大数据项目中有很广 ...

  4. YTU 2974: C语言习题5.26--文件操作3

    2974: C语言习题5.26--文件操作3 时间限制: 1 Sec  内存限制: 128 MB 提交: 213  解决: 92 题目描述 文本文件score.dic 中存储了n名学生的信息(班级编号 ...

  5. YTU 2973: C语言习题5.25--文件操作2

    2973: C语言习题5.25--文件操作2 时间限制: 1 Sec  内存限制: 128 MB 提交: 242  解决: 105 题目描述 文本文件score.dic 中存储了n名学生的信息(班级编 ...

  6. YTU 2972: C语言习题5.24--文件操作1

    2972: C语言习题5.24--文件操作1 时间限制: 1 Sec  内存限制: 128 MB 提交: 248  解决: 94 题目描述 文本文件score.dic 中存储了n名学生的信息(班级编号 ...

  7. Qt 多线程与数据库操作需要注意的几点问题(QSqlDatabase对象只能在当前线程里使用)

    彻底抛弃MFC, 全面应用Qt 已经不少时间了.除了自己看书按步就班做了十几个验证性的应用,还正式做了3个比较大的行业应用,总体感觉很好.Native C++ 下, Qt 基本是我用过的最简便的界面库 ...

  8. Qt 多线程与数据库操作需要注意的几点问题

    源地址:http://blog.csdn.net/goldenhawking/article/details/10811409 彻底抛弃MFC, 全面应用Qt 已经不少时间了.除了自己看书按步就班做了 ...

  9. C语言对mysql数据库的操作

    原文:C语言对mysql数据库的操作 这已经是一相当老的话题.不过今天我才首次使用,把今天的一些体会写下来,也许能给一些新手带来一定的帮助,更重要的是供自己今后忘记的怎么使用而进行查阅的! 我们言归正 ...

随机推荐

  1. virtualbox虚拟机上安装centOS的网络配置(安装centos时选择桥接网络)

    最近接触hadoop,需要在在Linux上面开发,所以我装了一个virtualbox虚拟机,在该虚拟机上面安装了一个centOS系统.linux系统是装好了,但是网络配置却另人头疼.我主要是想让宿主机 ...

  2. font-face字体文件引入方式

    <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8&quo ...

  3. HTML5自学笔记[ 15 ]canvas绘图实例之钟表

    <!doctype html> <html> <head> <meta charset="utf-8"> <title> ...

  4. BluetoothAdapter.LeScanCallback 参考文档

      BluetoothAdapter.LeScanCallback 参考文档   [翻译自: android开发文档] Class Overview:回调接口被用于传输LE扫描后的结果; 详情参看: ...

  5. 定向转发和重定向实现 <select >下拉表单数据传送

    定向转发的特点:   (1). 实行转发时浏览器上的网址不变  (如果你这点忽视了,那你就要接受我无尽的鄙视吧! 哇咔咔~~~)    (2). 实行转发时 :   只有一次请求.  不信,看这下面的 ...

  6. Java之线程———GUI线程(包含打字游戏和计时器俩个GUI实列)

    当java程序包含图形用户界面(GUI)时,Java虚拟机在运行应用程序时会自动启动更多的线程,其中有两个重要的线程:AWT-EventQuecue 和 AWT-Windows. AWT-EventQ ...

  7. 转:Struts2<s:iterator value="" var="lst">中var的使用和一些标签的使用体会

    比如<s:iterator value="pmOperateList" var="lst"> <!-- iterator加上var 等价于重新 ...

  8. org.hibernate.LazyInitializationException: could not initialize proxy - no Session

    原因:在延迟加载的状态下,使用某个属性时,但session已经关闭. 解决方法: 1.把load改成get,直接加载所有属性. 2.获取对象进行一次判断,如果对象没有初始化,就进行一次初始化. if ...

  9. CRM创建物料FM1

    这是在中联混凝土那边搞的.... method create_prd.  data: lt_return type bapiret2_tab,        ls_return like line o ...

  10. DataProcessing

    clear load X4058 [m,n]=size(X528); Mean=zeros(1,n); Dev=zeros(1,n); for i=1:n Xi=X528(1:end-1,i); Xi ...