您的位置:首页 > 移动开发 > Swift

SWIFT语言之多线程操作和操作队列(下)

2016-07-02 17:28 495 查看
转自:http://www.cnblogs.com/JackieHoo/p/4969309.html

如有侵犯,请来信oiken@qq.com

欢迎有兴趣的朋友,参与我的美女同事发起的活动《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)

}

}

}


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



 
 

欢迎大家关注我的微信公众号:丁丁的coding日记
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: