Почти каждый из нас испытывал дискомфорт, когда при нажатии на кнопку или при вводе текста в iOS или Mac приложениях, как внезапно пользовательский интерфейс переставал отвечать, приложение будто замирало и переставало реагировать на ваши действия.

На Mac, пользователи видит песочные часы или вращающийся радужный курсор, пока снова не возобновится взаимодействие с пользовательским интерфейсом. Многие пользователи ошибочно предполагают, что приложения сразу же заработает при прикосновению к экрану. Такие приложение дают ощущение низкой производительности и кажутся медленными, и, как правило, получает плохие отзывы в AppStore.

Но обеспечить постоянную отзывчивость пользовательского интерфейса не так уж просто. Как только приложение выполняет множество задач, оно начинает подтормаживать. Не так уж много времени выделяется для выполнения большого количества задач в главном потоке и для обеспечения отзывчивости интерфейса.

Что же должен делать «несчастный» разработчик? Решение есть, оно состоит в том, чтобы отделить основной поток через параллелизм. Параллелизм, это свойство приложения выполнять задачи в несколько потоков одновременно – и при этом пользовательский интерфейс остается отзывчивым, поскольку Вы выполняете свою работу в разных потоках.

Одним из методов для одновременного выполнения операций в iOS являются использование классов NSOperation и NSOperationQueue. В этой статье вы узнаете, как их использовать! Вы будете работать с приложением, которое совсем не использует многопоточность, таким образом, оно будет очень медленным, и будет «тормозить». Когда вы измените приложение, чтобы можно было добавлять параллельные операций и — надеюсь – это обеспечит более отзывчивый интерфейс для пользователя!

Приступим

Цель этой статьи состоит в том, чтобы показать таблицу с изображениями, к которым применили фильтр. Изображение будет загружено из интернета, к которому будет применен фильтр, а затем отображено в таблице.

Вот схематическое представление модели приложения:

image

Первая попытка

От автора: Так как статья была написана ранее 09.09.2015 для написания примеров был использован Swift версий 1.2. Я внес в примеры некоторые изменения с связи с выходом новой версий языка Swift. Исходный код на Swift 2.0 находиться по ссылкам: «стартовый» проект и «финальная» версия проекта.

Загрузите «стартовый» проект на Swift 1.2 и Swift 2.0, над которым вы будете работать по ходу прочтения этого мануала.

Примечание: Все изображения взяты из stock.xchng.Так как существуют случаи, когда изображение не удается загрузить, то для осуществления этой неудавшейся попытки некоторые изображения в источнике данных преднамеренно неверно названы.

Скомпилируйте и запустите приложение, и (в конечном счете) Вы увидите, что приложение отображает список фотографий. Попробуйте прокрутить список. Раздражает, не так ли?

image

Все действие происходит в файле ListViewController.swift, и большинство действий которые находится внутри метода делегата таблицы tableView(_:cellForRowAtIndexPath:).

Взгляните на этот метод и обратите внимание на два происходящих действия, которые довольно объемные:

  1. Загрузка изображений из сети. Даже если это — простая работа, приложение должно ожидать завершения загрузки, прежде чем оно сможет продолжать выполнять другую операцию.
  2. Применение фильтра к изображению с помощью Core Image. Этот метод применяет фильтр сепии к изображению. Если хотите узнать больше о Core Image filters, прочитайте Beginning Core Image in Swift.


Кроме того, Вы также загружаете список фотографий из сети, при первом запросе:

lazy var photos = NSDictionary(contentsOfURL:dataSourceURL)

Вся эта работа происходит в основном потоке приложения. Поскольку основной поток также отвечает за взаимодействие с пользователем, то загрузка и применение фильтра к изображениям снижает способность к быстрому реагированию на действия пользователя. Вы можете убедиться в этом с помощью вкладки индикаторов в среде разработки Xcode. Вы можете добраться к этой закладке, нажав на Debug navigator (Command-6) и затем выбрав CPU, во время выполнения приложения.

image

Вы увидите все эти пики в первом потоке, который является основным потоком приложения. Для получения более подробной информации, запустите приложение с использование Инструментов, но это уже совсем другая история.

Пора подумать о том, как можно улучшить ваш пользовательский опыт! И сделать приложение более отзывчивым.

Задачи, Потоки и Процессы

Прежде чем Вы погрузитесь в статью, есть несколько технических понятий, которые нужно рассмотреть. Я дам определение нескольким терминам:

  • Задача/Task: простая единица, которая должна быть выполнена.
  • Поток/Thread: механизм, созданный операционной системой, где выполняются несколько команд одновременно в одном приложении.
  • Процесс/Process: выполняющийся фрагмент программного кода, который может состоять из несколько потоков.


Примечание: В iOS и OS X, функциональность многопоточность обеспечивается средствами POSIX Threads API и является частью операционной системы. Это работа довольно низкого уровня, и вы поймете насколько легко можно наделать ошибок; возможно, худшее в использование потоков – это ошибки, которые невероятно сложно найти!

Foundation framework содержит класс под названием NSThread, с которым намного проще работать, но управление несколькими потоками с помощью NSThread все еще вызывает трудности. NSOperation и NSOperationQueue высокоуровневые классы, которые значительно упрощают процесс работы с несколькими потоками.

В этой схеме Вы видите отношение между процессом, потоками и задачами:

image

Как Вы видите, процесс может содержать несколько потоков, и каждый поток может выполнять несколько задач поочередно.

В этой схеме, второй поток выполняет чтения файла, в то время как первый поток выполняет код, связанный с пользовательским интерфейсом. Это очень похоже на то, как вы должны структурировать свой код в iOS – основной поток должен выполнять любую работу, связанную с пользовательским интерфейсом, а вторичные потоки должны выполнять медленные или длительные операции, такие как чтение файлов, доступ к сети, и т.д.

NSOperation против Grand Central Dispatch (GCD)

Возможно, Вы слышали о Grand Central Dispatch (GCD). В общих чертах GCD состоит из языковых особенностей, runtime библиотек и расширений системы, что в свою очередь обеспечивает системные всесторонние улучшения для поддержания параллелизма на многоядерных мобильных устройствах в iOS и OS X. Если Вы хотите больше узнать о GCD, Вы можете прочитать Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial.

NSOperation и NSOperationQueue построенные поверх GCD. Стоит заметить что, Apple рекомендует использовать абстракцию высшего уровня, и затем опуститься к нижним уровням, когда измерения показывают, что они необходимы.

Вот краткое сравнение, которое, поможет Вам решить, когда и где использовать GCD или NSOperation:
  • GCD это легковесный способ представление единиц работы, которые будут выполняться одновременно. Ненужно вносить в список эти единицы работы; система сама позаботится об этом за Вас. Добавление зависимости среди блоков может быть не легким заданием. Отмена или приостановка блока создает дополнительную работу для Вас как разработчику!
  • NSOperation добавляет небольшие дополнительные издержки по сравнению с GCD, но Вы можете добавить зависимость для различных операций, повторно использовать, отменить или приостановить их.


В этом туториале мы будем использовать класс NSOperation, потому что Вы имеете дело с табличным представлением и по причине производительности, и энергопотребления, Вам необходимо отменить операцию для определенного изображения, если пользователь прокрутил изображение за пределы экрана. Даже если операции будут находиться в фоновом потоке, если десятки таких операций будут ожидать своей очереди для выполнения, то производительность будет по-прежнему не на том уровне что нам необходимо.

Изменение схемы модели приложения

Пора изменить предварительную без многопоточности модель приложения! Если Вы более внимательно присмотритесь на предварительную модель, Вы увидите, что есть три проблематичных области, которые можно улучшить. Разделяя эти три области и вынести их в отдельный поток, основной поток будет освобожден, и будет способный быстро реагировать на взаимодействие пользователя с UI.

image

Чтобы избавиться от узких мест в приложении, Вам будет нужен поток, который будет отвечать на взаимодействие пользователя с UI, ещё поток, который будет предназначенный для загрузки изображений и ещё один поток для выполнения применения фильтра к изображению. В новой модели приложение запускается в основном потоке и загружает пустое табличное представление. Одновременно, приложение запускает второй поток для загрузки данных.

Как только данные загрузиться, необходимо обновить таблицу. Это должно быть сделано в основном потоке, так как он включает в себя пользовательский интерфейс. На данном этапе табличное представление знает, сколько у него есть ячеек, и какие URL изображений должны быть отображены на экран, но у него еще нет никаких фактических изображений! Если Вы сразу начнете загружать все изображения в один момент, то это будет совсем неэффективно, поскольку Вам не нужны все изображения сразу!

Что можно сделать для улучшения?

Усовершенствованная модель начнет загружать изображения, которые соответствуют отображаемым на экране ячейкам. Таким образом, Ваш код сначала узнает, какие ячейки видимы, и только тогда начнется процесс загрузки. Также, процесс применения фильтра для изображения не может быть начат, прежде чем изображение не будет полностью загружено. Поэтому, код который отвечает за применение фильтра не должен запускать, пока нет изображение для обработки.

Для лучшей отзывчивости приложения, мы должны отображать изображение, как только оно загрузиться. Тогда мы начнет работу с изображениям, и затем обновит интерфейс, чтобы вывести на экран обработанные изображение. На схеме ниже схематически показано поток управления для этого процесса:

image

Чтобы достигнуть этих целей, Вы должны будете отследить, загружается ли изображение в настоящее время, или уже загружено, или был ли применен фильтр к изображению. Вам нужно будет также отследить состояние каждой операции, и является ли это загрузкой или обработкой изображения так, чтобы Вы могли отменить, приостановить или возобновить её.

Хорошо! Теперь можно приступить к самому интересному, реализаций нашей схемы!

Откройте проект и добавьте новый Swift File к проекту с именем 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
  }
}

Примечание: Убедитесь, что вы сделали импорт UIKit в начало файла. По умолчанию Xcode делает импорт только Foundation в Swift’е.

Этот класс будет представлять каждую фотографию, отображаемую в приложении, вместе с её текущим состоянием, который установлен по умолчанию как .New для недавно создаваемых записей. Изображением по умолчанию установлена картинка под названием «Placeholder».

Чтобы отследить состояние каждой операции, Вам будет нужен отдельный класс. Добавьте следующую реализацию в конец 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
    }()
}

Этот класс содержит два словаря, чтобы отслеживать активную и незавершенную загрузку изображений и операции применения фильтра для каждой ячейки в таблице и две очереди операций для каждого типа операций.

Все значения создаются медленно, то есть они не могут быть инициализированными, пока они не получат доступ. Это улучшит производительность вашего приложения.

Создание класса NSOperationQueue очень простое. Именование Ваших очередей помогает их потом легко найти в Инструментах или в отладчике. MaxConcurrentOperationCount установлен в значение 1, но только для этой статьи, чтобы Вы увидели, что операции завершаются одна за другой. Вы можете пропустить эту часть, чтобы позволить очереди решать, сколько операций она может обработать сразу – это еще больше повысит производительность.

Как же очередь решает, сколько операций она может запустить одновременно? Это хороший вопрос! Это зависит от аппаратного обеспечения. По умолчанию, NSOperationQueue делает некоторые вычисления за «кулисами», чтобы решить, что лучше для конкретной платформы, на которой выполняется код, и запустить максимально возможное число потоков.

Рассмотрим следующий пример. Предположим, система неактивна, и есть много доступных ресурсов, таким образом, очередь может запустить приблизительно восемь одновременных потоков. В следующий раз, когда вы запустите приложение, система может быть занята другими операциями, которые используют ресурсы, и очередь запустит только два одновременных потока. Поскольку Вы установили максимальное параллельное количество операций, то в этом приложении будет выполняться только одна операция.

Примечание: Вы можете задаться вопросом, почему Вы должны отслеживать все активные и незаконченные операции. У очереди есть метод operations, который возвращает массив операций, так почему бы не использоваться этим? В этом проекте его нет смысла использовать Вы должны отслеживать, с какими операциями связанные ячейки таблицы, которые включают в себя процесс итерации по массиву каждый раз, когда Вам это нужно. Хранение их в словаре с индексом в качестве основного средства поиска, является быстрым и эффективным способом.

Пора позаботиться об операциях применения фильтра к изображению и загрузки самого изображения. Добавьте следующий код в конец PhotoOperations.swift:

class ImageDownloader: NSOperation {
  //1
  let photoRecord: PhotoRecord
 
  //2
  init(photoRecord: PhotoRecord) {
    self.photoRecord = photoRecord
  }
 
  //3
  override func main() {
    //4
    if self.cancelled {
      return
    }
    //5
    let imageData = NSData(contentsOfURL:self.photoRecord.url)
 
    //6
    if self.cancelled {
      return
    }
 
    //7
    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, это абстрактный класс, разработанный для создания производного класса. Каждый производный класс представляет собой конкретную задачу, как показано на рисунке выше.

Вот то, что происходит на каждом из пронумерованных комментариев в коде:
  1. Добавьте постоянную ссылку к объекту PhotoRecord, относящуюся к операции.
  2. Создайте указываемый инициализатор позволяющий передавать записи фото.
  3. main является методом, который Вы переопределяете в производных классах NSOperation для выполнения работы. Вы делаете пул автовыпуска, так как Вы работаете вне пула, создаваемого основным потоком.
  4. Проверьте на отмену перед запуском. Операции должны регулярно проверять были ли они отменены перед долгой и интенсивной работой.
  5. Загрузите изображение.
  6. Проверьте на отмену.
  7. Если есть данные, создайте объект изображения, и добавьте его к записи и переместите состояние вперед. Если нет никаких данных, отметьте запись, как неудавшеюся и установите соответствующее изображение


Далее, вы создадите еще ??одну операцию, чтобы следить за применением фильтра! Добавьте следующий код в конец PhotoOperations.swift:

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и удалите объявления свойств lazy var photos. Затем добавьте следующие объявления вместо этого:

var photos = [PhotoRecord]()
let pendingOperations = PendingOperations()

Они будут содержать массив объектов PhotoDetails, которые Вы создали ранее, и объект 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 {
                
                let datasourceDictionary = (try! NSPropertyListSerialization.propertyListWithData(data!, options:NSPropertyListMutabilityOptions.MutableContainersAndLeaves, 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()
            }
            
            if error != nil {
                let alert = UIAlertView(title:"Oops!",message:error!.localizedDescription, delegate:nil, cancelButtonTitle:"OK")
                alert.show()
            }
            UIApplication.sharedApplication().networkActivityIndicatorVisible = false
        }
 }

Этот метод создает асинхронный запрос, который, по окончанию, выполнит комплишен блок в основном потоке. Когда загрузка завершена, данные будут извлечены из NSDictionary и затем будет снова обработан в массив объектов PhotoRecord. Вы не использовали NSOperation непосредственно, но вместо этого Вы получили доступ к основной очереди, используя NSOperationQueue.mainQueue().

Вызовите новый метод в конце viewDidLoad:

fetchPhotoDetails ()

Затем, найдите метод tableView(_:cellForRowAtIndexPath:) и замените следующей реализацией:

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("CellIdentifier", forIndexPath: indexPath)
        
        //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()
            
            if (!tableView.dragging && !tableView.decelerating) {
                self.startOperationsForPhotoRecord(photoDetails, indexPath: indexPath)
            }
        }
        return cell
}

Найдите время и прочитайте объяснения отмеченных разделов ниже:

  1. Чтобы предоставить обратную связь пользователю, создайте UIActivityIndicatorView и установите ее в качестве представление аксессуара ячейки.
  2. Источник данных содержит экземпляры PhotoRecord. Выберите один правильный, основываясь на текущей строке indexPath.
  3. Текстовая метка ячейки — (почти) всегда одинакова, и изображение устанавливается соответственно на PhotoRecord, поскольку она обрабатывается, таким образом, Вы можете установить их обоих здесь, независимо от состояния записи.
  4. Осмотрите запись. Настройте индикатор активности и текст по мере необходимости, и начните операции (еще не реализованные).


Вы можете удалить реализацию метода 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")
        }
}

Здесь, вы передадите экземпляр PhotoRecord вместе с индексом. В зависимости от состояния фото Вы начнете загрузку фотографий или применение фильтра к изображению.

Примечание: методы загрузки и применения фильтра к изображению реализуются отдельно, так как есть вероятность того, что в то время как изображение загружается, пользователь может далеко его прокрутить, а вы не еще ??применили фильтр изображения. Так что в следующий раз, когда пользователь зайдет на той же ряд, вам не нужно повторно загружать изображение; вам нужно только применить фильтр к изображению! Эффективность рулит!

Теперь Вы должны реализовать методы, которые Вы вызывали в методе выше. Помните, что Вы создали пользовательский класс, PendingOperations, чтобы отслеживать операции; теперь принимайтесь за использование! Добавьте следующие методы к классу:

func startDownloadForRecord(photoDetails: PhotoRecord, indexPath: NSIndexPath){
        //1
        if let _ = 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 _ = 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)
 }

Хорошо! Вот краткий список, чтобы убедиться, что вы понимаете, что происходит в коде который вы добавили только что:
  1. Прежде всего, проверьте на наличие конкретного indexPath чтобы увидеть, есть ли уже операция в downloadsInProgress для него. Если это так, проигнорируйте его.
  2. Если нет, создайте экземпляр ImageDownloader, используя указываемый инициализатор.
  3. Добавьте блок завершения, который будет выполняться, когда операция будет завершена. Это — неплохое место, где могла бы завершиться операция. Важно отметить, что блок завершения выполняется, даже если операция отменена, таким образом, Вы должны проверить это свойство в первую очередь. Вы также не знаете точно, через который поток перенаправляется завершения блока, таким образом, Вы должны использовать GCD, чтобы инициировать перезагрузку табличного представления на основном потоке.
  4. Добавьте операцию к downloadsInProgress, чтобы отслеживать действия.
  5. Добавьте операцию в очередь загрузки. Таким образом, вы запускаете операции – очередь следит за планированием, как только Вы добавляете операцию.


Метод применения фильтра к изображениям происходит таким же образом, за исключением использования ImageFiltration и filtrationsInProgress для отслеживания операции. В качестве упражнения, вы можете попробовать избавиться от дублирования кода в этой секции.

Поздравляю! Ваша работа завершена. Скомпилируйте и запустите приложение, и Вы увидите сделанные улучшения в действии! При прокрутке таблицы, приложение не будет больше притормаживать, и начнете загружать изображения и применять фильтры к ним, как только они будут загружены, и отображаемым ячейкам.

image

Разве это не круто? Всего немного усилий и приложение стало более отзывчивым – и пользователь получит море положительных эмоций!

Тонкая настройка

Вы узнали много полезного из этой статьи! Ваш небольшой проект стал более отзывчивым и показывает лучшую производительность по сравнению с оригинальной версией. Однако есть все еще некоторые маленькие детали, над которыми еще нужно поработать. Вы же хотите стать великим программистом, не только хорошим!

Вы, возможно, заметили, что при прокрутке таблицы, ячейки которые ушли с поля видимости все еще загружаются и причиняют фильтр к изображению. Если вы прокрутите быстро, приложение будет занято загрузкой и фильтрацией изображения для ячеек дальше по списку, даже если они не видны. В идеале, приложение должно отменить операцию для ячеек которые не видны и дать приоритет ячейкам, которые отображаются в данный момент.

Разве Вы не реализовали методы для отмены загрузки и наложения фильтра в своем коде? Да, Вы сделали это — теперь Вам следует использовать их!

Вернитесь к Xcode и откройте файл ListViewController. Перейдите к реализации метода tableView(_:cellForRowAtIndexPath:), и перенесите вызов метода startOperationsForPhotoRecord с использование логического оператор следующим образом:

if (!tableView.dragging && !tableView.decelerating) {
  self.startOperationsForPhotoRecord(photoDetails, indexPath: indexPath)
}

Вы сообщаете таблице запустить операции, только если таблица не скроллится. На самом деле, это свойства класса UIScrollView, и потому что класс UITableView является наследником UIScrollView, он автоматически наследуете эти свойства.

Затем добавьте реализацию следующего метода делегата UIScrollView в классе:

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()
}

Быстрое пошаговое руководство вышеупомянутого кода показывает следующее:

  1. Как только пользователь начинает прокручивать, Вы захотите приостановить все операции и взглянуть на то, что хочет видеть пользователь. Вы осуществите suspendAllOperations через минуту.
  2. Если значение замедления ложное, то значит, что пользователь прекратил перетаскивать табличное представление. Поэтому необходимо возобновить приостановленные операции, отменить операций для внеэкранных ячеек, и начать операции для экранных ячеек. Вы будете осуществлять loadImagesForOnscreenCells и resumeAllOperations через некоторое время также.
  3. Этот метод делегата сообщит Вам, что табличное представление прекратило прокручиваться, так что вам нужно опять следовать пункту #2.


Теперь добавьте реализацию этих недостающих методов к ListViewController.swift:

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)
            allPendingOperations.unionInPlace(pendingOperations.filtrationsInProgress.keys)
            
            //3
            var toBeCancelled = allPendingOperations
            let visiblePaths = Set(pathsArray)
            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)
            }
        }
 }


suspendAllOperations и resumeAllOperations имеют простейшую реализацию. NSOperationQueues может быть приостановлен, установив свойство suspended к true. Оно будет приостанавливать все операции в очереди, и Вы не сможете приостановить операции индивидуально.

loadImagesForOnscreenCells немного более сложен. Вот что происходит:

  1. Начните с массива, содержащего индексные пути всех видимых строк в данный момент в табличном представлении.
  2. Создайте ряд всех незаконченных операций, объединив все загрузки + все фильтры.
  3. Создайте ряд всех индексных путей с операциями, которые будут отменены. Начните со всех операций, и затем удалите индексные пути видимых строк. Это оставит набор операций, включающих внеэкранныее строки.
  4. Создайте ряд индексных путей, которым нужны их запущенные операции. Начните со всех видимых строк индексных путей, и затем удалите те, где операции ждут своей очереди
  5. Пройдитесь в цикле по тем операциям, которые будут отменены, отмените их, и удалите их ссылку с PendingOperations.
  6. Пройдитесь в цикле по тем операциям, которые будут запущены, и вызовите startOperationsForPhotoRecord для каждой из них.


Скомпилируйте и запустите приложение, и у вас должно получиться еще более отзывчивое и лучшее в управлении ресурсами приложения!

image

Заметьте, что, когда Вы заканчиваете скроллить таблицу, изображения на видимых ячейках сразу же обрабатываться.

И что же дальше?

Это «финальная» версия проекта на Swift 1.2. b и Swift 2.0

Если Вы завершили этот проект и уделили время, чтобы действительно понять его, тогда поздравляем! Вы можете считать себя намного более ценным iOS разработчиком, чем Вы были в начале этой статьи! Большинство компаний, которые специализируются на разработке аппаратного обеспечения, могут гордиться, если у них есть один или два человека, которые действительно знаются на этом.

Но будьте осторожны, безвозмездное использование многопоточности может сделать проект нечитаемым для других разработчиков, которые должны будут поддерживать ваш код. Потоки могут представлять трудно находимые ошибки в программе, которые могут никогда не появляться, пока к Вас хороший интернет, или код запущен на более быстром (или медленном) устройстве. Тестируйте очень тщательно, и всегда использует Инструменты (или Ваши собственные наблюдения), чтобы проверить, что представление потоков действительно улучшились.

Полезная функция для операций, о которой здесь не рассказывается, является зависимость. Вы можете сделать операцию зависящей от одной или более других операций. Эта операция не запустится, пока операции, от которых она зависит, не завершатся. Например:

// MyDownloadOperation is a subclass of NSOperation
let downloadOperation = MyDownloadOperation()

// MyFilterOperation  is a subclass of NSOperation
let filterOperation = MyFilterOperation()

filterOperation.addDependency(downloadOperation)

Для удаления зависимости:

filterOperation.removeDependency(downloadOperation)

Можно ли упростить и улучшить код в этом проекте с помощью зависимостей? Примените Ваши навыки на практике и испробуйте это сделать. Важно отметить, что зависимая операция все равно будет запускаться, даже если операции, от которых она зависит, будут отменены, а также если они завершатся естественно. Вы должны принять это во внимание.

Комментарии (1)


  1. InstaRobot
    03.11.2015 21:30

    Очень и очень нужный материал. Использую операции в объектном С, но переход на Свифт не за горами. Однозначно в «Избранное»!