zoukankan      html  css  js  c++  java
  • 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 = 1
    
        return queue
    
        }()
    
     
    
      lazy var filtrationsInProgress = [NSIndexPath:NSOperation]()
    
      lazy var filtrationQueue:NSOperationQueue = {
    
        var queue = NSOperationQueue()
    
        queue.name = "Image Filtration queue"
    
        queue.maxConcurrentOperationCount = 1
    
        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 > 0 {
    
          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
    
     
    
      //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()
    
        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){
    
      //1
    
      if let downloadOperation = pendingOperations.downloadsInProgress[indexPath] {
    
        return
    
      }
    
     
    
      //2
    
      let downloader = ImageDownloader(photoRecord: photoDetails)
    
      //3
    
      downloader.completionBlock = {
    
        if downloader.cancelled {
    
          return
    
        }
    
        dispatch_async(dispatch_get_main_queue(), {
    
          self.pendingOperations.downloadsInProgress.removeValueForKey(indexPath)
    
          self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
    
        })
    
      }
    
      //4
    
      pendingOperations.downloadsInProgress[indexPath] = downloader
    
      //5
    
      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) {
    
      //1
    
      suspendAllOperations()
    
    }
    
     
    
    override func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) {
    
      // 2
    
      if !decelerate {
    
        loadImagesForOnscreenCells()
    
        resumeAllOperations()
    
      }
    
    }
    
     
    
    override func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
    
      // 3
    
      loadImagesForOnscreenCells()
    
      resumeAllOperations()
    
    }
    
     
    
     
    
    func suspendAllOperations () {
    
      pendingOperations.downloadQueue.suspended = true
    
      pendingOperations.filtrationQueue.suspended = true
    
    }
    
     
    
    func resumeAllOperations () {
    
      pendingOperations.downloadQueue.suspended = false
    
      pendingOperations.filtrationQueue.suspended = false
    
    }
    
     
    
    func loadImagesForOnscreenCells () {
    
      //1
    
      if let pathsArray = tableView.indexPathsForVisibleRows() {
    
        //2
    
        var allPendingOperations = Set(pendingOperations.downloadsInProgress.keys.array)
    
        allPendingOperations.unionInPlace(pendingOperations.filtrationsInProgress.keys.array)
    
     
    
        //3
    
        var toBeCancelled = allPendingOperations
    
        let visiblePaths = Set(pathsArray as! [NSIndexPath])
    
        toBeCancelled.subtractInPlace(visiblePaths)
    
     
    
        //4
    
        var toBeStarted = visiblePaths
    
        toBeStarted.subtractInPlace(allPendingOperations)
    
     
    
        // 5
    
        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)
    
        }
    
     
    
        // 6
    
        for indexPath in toBeStarted {
    
          let indexPath = indexPath as NSIndexPath
    
          let recordToProcess = self.photos[indexPath.row]
    
          startOperationsForPhotoRecord(recordToProcess, indexPath: indexPath)
    
        }
    
      }
    
    }

     

     

    这已经是最后一步了,恭喜你,也辛苦你了,不过这是值得的,现在你运行看看,一个响应用户及时,并且资源管理合理的程序就在你手上诞生了。注意一下当你滚动表格结束,可见的表格行将马上开始处理操作。 

     

     

  • 相关阅读:
    图像的卷积
    信息理论与编码中有关信源编码的笔记
    Java 数组排序
    完全平方数
    Java 作业题4
    Java 作业题3
    Java 作业题 2
    算法面试题二:旋转数组,存在重复元素,只出现一次的数字
    算法面试题一:排序算法及贪心算法
    微信小程序 发送模板消息的功能实现
  • 原文地址:https://www.cnblogs.com/JackieHoo/p/4969309.html
Copyright © 2011-2022 走看看