Трюки с производительностью iOS, чтобы сделать ваше приложение более производительным
Опубликовано: 2022-03-10Хотя современное оборудование iOS достаточно мощное, чтобы справляться со многими ресурсоемкими и сложными задачами, устройство все равно может перестать отвечать на запросы, если вы не будете внимательно следить за тем, как работает ваше приложение. В этой статье мы рассмотрим пять приемов оптимизации, которые сделают ваше приложение более отзывчивым.
1. Удаление многоразовой ячейки из очереди
Вероятно, вы использовали tableView.dequeueReusableCell(withIdentifier:for:)
внутри tableView(_:cellForRowAt:)
раньше. Вы когда-нибудь задумывались, почему вы должны следовать этому неудобному API, а не просто передавать массив ячеек? Давайте рассмотрим аргументацию этого.
Скажем, у вас есть табличное представление с тысячей строк. Без использования повторно используемых ячеек нам пришлось бы создавать новую ячейку для каждой строки, например так:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { // Create a new cell whenever cellForRowAt is called. let cell = UITableViewCell() cell.textLabel?.text = "Cell \(indexPath.row)" return cell }
Как вы могли подумать, это добавит тысячу ячеек в память устройства при прокрутке вниз. Представьте, что произойдет, если каждая ячейка будет содержать UIImageView
и много текста: загрузка их всех сразу может привести к тому, что приложению не хватит памяти! Кроме того, каждая отдельная ячейка потребует выделения новой памяти во время прокрутки. Если вы быстро прокручиваете табличное представление, на лету будет выделяться много небольших фрагментов памяти, и этот процесс сделает пользовательский интерфейс дерганным!
Чтобы решить эту проблему, Apple предоставила нам метод dequeueReusableCell(withIdentifier:for:)
. Повторное использование ячеек работает путем помещения ячейки, которая больше не отображается на экране, в очередь, и когда новая ячейка должна быть видна на экране (скажем, следующая ячейка ниже, когда пользователь прокручивает вниз), представление таблицы будет получить ячейку из этой очереди и изменить ее в cellForRowAt indexPath:
Используя очередь для хранения ячеек, в табличном представлении не нужно создавать тысячу ячеек. Вместо этого ему нужно ровно столько ячеек, чтобы покрыть область табличного представления.
Используя dequeueReusableCell
, мы можем уменьшить объем памяти, используемой приложением, и сделать его менее склонным к нехватке памяти!
2. Использование экрана запуска, который выглядит как начальный экран
Как упоминалось в Руководстве Apple по человеческому интерфейсу (HIG), экраны запуска можно использовать для улучшения восприятия отзывчивости приложения:
«Он предназначен исключительно для улучшения восприятия вашего приложения как быстрого запуска и немедленной готовности к использованию. Каждое приложение должно иметь экран запуска».
Распространенной ошибкой является использование экрана запуска в качестве экрана-заставки для демонстрации фирменного стиля или добавления анимации загрузки. Спроектируйте экран запуска так, чтобы он был идентичен первому экрану вашего приложения, как указано Apple:
«Разработайте экран запуска, который почти идентичен первому экрану вашего приложения. Если вы включаете элементы, которые выглядят по-разному после завершения запуска приложения, люди могут столкнуться с неприятной вспышкой между экраном запуска и первым экраном приложения.
«Экран запуска не является возможностью брендинга. Не создавайте интерфейс входа, который выглядит как заставка или окно «О программе». Не включайте логотипы или другие элементы брендинга, если только они не являются статической частью первого экрана вашего приложения».
Использование экрана запуска для загрузки или брендинга может замедлить время первого использования и заставить пользователя почувствовать, что приложение работает вяло.
Когда вы начинаете новый проект iOS, будет создан пустой LaunchScreen.storyboard
. Этот экран будет показан пользователю, пока приложение загружает контроллеры представления и макет.
Чтобы ваше приложение работало быстрее, вы можете спроектировать экран запуска так, чтобы он был похож на первый экран (контроллер представления), который будет показан пользователю.
Например, экран запуска приложения Safari похож на его первый вид:
Раскадровка экрана запуска похожа на любой другой файл раскадровки, за исключением того, что вы можете использовать только стандартные классы UIKit, такие как UIViewController, UITabBarController и UINavigationController. Если вы попытаетесь использовать какие-либо другие пользовательские подклассы (например, UserViewController), Xcode уведомит вас о том, что использование имен пользовательских классов запрещено.
Следует также отметить, что UIActivityIndicatorView
не анимируется при размещении на экране запуска, поскольку iOS создает статическое изображение из раскадровки экрана запуска и отображает его пользователю. (Это кратко упоминается в презентации WWDC 2014 «Состояние платформ в Союзе» около 01:21:56
.)
HIG от Apple также советует нам не включать текст на наш экран запуска, потому что экран запуска статичен, и вы не можете локализовать текст для разных языков.
Рекомендуемая литература : Мобильное приложение с функцией распознавания лиц: как сделать это реальным
3. Восстановление состояния для контроллеров представления
Сохранение и восстановление состояния позволяют пользователю вернуться к тому же самому состоянию пользовательского интерфейса, которое было непосредственно перед выходом из приложения. Иногда из-за нехватки памяти операционной системе может потребоваться удалить ваше приложение из памяти, пока оно находится в фоновом режиме, и приложение может потерять отслеживание своего последнего состояния пользовательского интерфейса, если оно не сохранено, что может привести к тому, что пользователи потеряют свою работу. в ходе выполнения!
На экране многозадачности мы видим список приложений, которые были переведены в фоновый режим. Можно предположить, что эти приложения все еще работают в фоновом режиме; на самом деле некоторые из этих приложений могут быть уничтожены и перезапущены системой из-за нехватки памяти. Снимки приложения, которые мы видим в режиме многозадачности, на самом деле являются снимками экрана, сделанными системой сразу после выхода из приложения (т. е. для перехода на главный экран или экран многозадачности).
iOS использует эти снимки экрана, чтобы создать иллюзию того, что приложение все еще работает или все еще отображает это конкретное представление, в то время как приложение могло быть уже завершено или перезапущено в фоновом режиме, но все еще отображает тот же снимок экрана.
Вы когда-нибудь сталкивались с тем, что после возобновления работы приложения с экрана многозадачности приложение показывает пользовательский интерфейс, отличный от моментального снимка, показанного в режиме многозадачности? Это связано с тем, что в приложении не реализован механизм восстановления состояния, а отображаемые данные были потеряны, когда приложение было закрыто в фоновом режиме. Это может привести к нежелательным последствиям, поскольку пользователь ожидает, что ваше приложение будет в том же состоянии, в котором он его покинул.
Из статьи Apple:
«Они ожидают, что ваше приложение будет в том же состоянии, в котором они его оставили. Сохранение и восстановление состояния гарантирует, что ваше приложение вернется в прежнее состояние при повторном запуске».
UIKit проделывает большую работу, чтобы упростить для нас сохранение и восстановление состояния: он автоматически обрабатывает сохранение и загрузку состояния приложения в нужное время. Все, что нам нужно сделать, это добавить некоторую конфигурацию, чтобы указать приложению поддерживать сохранение и восстановление состояния и сообщить приложению, какие данные необходимо сохранить.
Чтобы включить сохранение и восстановление состояния, мы можем реализовать эти два метода в AppDelegate.swift
:
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool { return true }
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool { return true }
Это укажет приложению автоматически сохранять и восстанавливать состояние приложения.
Далее мы сообщим приложению, какие контроллеры представления необходимо сохранить. Мы делаем это, указав «ID восстановления» в раскадровке:
Вы также можете установить флажок «Использовать идентификатор раскадровки», чтобы использовать идентификатор раскадровки в качестве идентификатора восстановления.
Чтобы установить идентификатор восстановления в коде, мы можем использовать свойство restorationIdentifier
контроллера представления.
// ViewController.swift self.restorationIdentifier = "MainVC"
Во время сохранения состояния любой контроллер представления или представление, которому был назначен идентификатор восстановления, будет сохранять свое состояние на диск.
Идентификаторы восстановления можно сгруппировать вместе, чтобы сформировать путь восстановления. Идентификаторы сгруппированы с использованием иерархии представления, от корневого контроллера представления до текущего активного контроллера представления. Предположим, MyViewController встроен в навигационный контроллер, который встроен в другой контроллер панели вкладок. Предполагая, что они используют свои собственные имена классов в качестве идентификаторов восстановления, путь восстановления будет выглядеть следующим образом:
TabBarController/NavigationController/MyViewController
Когда пользователь покидает приложение с MyViewController, являющимся активным контроллером представления, этот путь будет сохранен приложением; тогда приложение запомнит предыдущую показанную иерархию представлений ( Контроллер панели вкладок → Контроллер навигации → Контроллер моего представления ).
После присвоения идентификатора восстановления нам нужно будет реализовать методы encodeRestorableState(with coder:) и decodeRestorableState(with coder:) для каждого из сохраненных контроллеров представления. Эти два метода позволяют указать, какие данные необходимо сохранить или загрузить и как их кодировать или декодировать.
Давайте посмотрим на контроллер представления:
// MyViewController.swift // MARK: State restoration // UIViewController already conforms to UIStateRestoring protocol by default extension MyViewController { // will be called during state preservation override func encodeRestorableState(with coder: NSCoder) { // encode the data you want to save during state preservation coder.encode(self.username, forKey: "username") super.encodeRestorableState(with: coder) } // will be called during state restoration override func decodeRestorableState(with coder: NSCoder) { // decode the data saved and load it during state restoration if let restoredUsername = coder.decodeObject(forKey: "username") as? String { self.username = restoredUsername } super.decodeRestorableState(with: coder) } }
Не забудьте вызвать реализацию суперкласса внизу вашего собственного метода. Это гарантирует, что родительский класс имеет возможность сохранять и восстанавливать состояние.
Как только объекты закончат декодирование, будет вызван applicationFinishedRestoringState()
, чтобы сообщить контроллеру представления, что состояние было восстановлено. Мы можем обновить пользовательский интерфейс для контроллера представления в этом методе.
// MyViewController.swift // MARK: State restoration // UIViewController already conforms to UIStateRestoring protocol by default extension MyViewController { ... override func applicationFinishedRestoringState() { // update the UI here self.usernameLabel.text = self.username } }
Вот оно! Это основные методы реализации сохранения и восстановления состояния вашего приложения. Имейте в виду, что операционная система удалит сохраненное состояние, когда приложение будет принудительно закрыто пользователем, чтобы избежать зависания в сломанном состоянии в случае, если что-то пойдет не так при сохранении и восстановлении состояния.
Кроме того, не сохраняйте никакие данные модели (то есть данные, которые должны были быть сохранены в UserDefaults или Core Data) в состоянии, даже если это может показаться удобным. Данные состояния будут удалены, когда пользователь принудительно покинет ваше приложение, и вы, конечно же, не хотите терять данные модели таким образом.
Чтобы проверить, хорошо ли работают сохранение и восстановление состояния, выполните следующие действия:
- Создайте и запустите приложение с помощью Xcode.
- Перейдите к экрану с сохранением и восстановлением состояния, которое вы хотите протестировать.
- Вернитесь на главный экран (смахнув вверх или дважды щелкнув кнопку «Домой» или нажав Shift ⇧ + Cmd ⌘ + H в симуляторе), чтобы перевести приложение в фоновый режим.
- Остановите приложение в Xcode, нажав кнопку.
- Запустите приложение еще раз и проверьте, успешно ли восстановлено состояние.
Поскольку в этом разделе рассматриваются только основы сохранения и восстановления состояния, я рекомендую следующие статьи Apple Inc. для получения более глубоких знаний о восстановлении состояния:
- Сохранение и восстановление состояния
- Процесс сохранения пользовательского интерфейса
- Процесс восстановления пользовательского интерфейса
4. Сократите использование непрозрачных представлений настолько, насколько это возможно
Непрозрачное представление — это представление, не имеющее прозрачности, что означает, что любой элемент пользовательского интерфейса, расположенный за ним, вообще не виден. Мы можем сделать представление непрозрачным в Interface Builder:
Или мы можем сделать это программно с помощью свойства isOpaque
UIView:
view.isOpaque = true
Установка непрозрачного вида заставит систему рисования оптимизировать производительность рисования при рендеринге экрана.
Если представление имеет прозрачность (т. е. альфа-канал ниже 1,0), iOS придется проделать дополнительную работу, чтобы вычислить, что должно отображаться, путем смешивания различных слоев представлений в иерархии представлений. С другой стороны, если вид установлен как непрозрачный, то система рисования просто поместит этот вид на передний план и избежит дополнительной работы по смешиванию нескольких слоев вида за ним.
Вы можете проверить, какие слои смешиваются (непрозрачные) в симуляторе iOS, выбрав Debug → Color Blended Layers .
Отметив параметр « Слои со смешанным цветом », вы увидите, что некоторые представления окрашены в красный цвет, а некоторые — в зеленый. Красный цвет указывает на то, что вид не является непрозрачным и что его выходной дисплей является результатом смешения слоев за ним. Зеленый цвет означает, что изображение непрозрачно и смешивание не выполнялось.
Ярлыки, показанные выше («Просмотреть друзей» и т. д.), выделены красным цветом, поскольку при перетаскивании ярлыка на раскадровку его фоновый цвет по умолчанию устанавливается прозрачным. Когда система рисования компонует изображение рядом с областью метки, она запросит слой за меткой и выполнит некоторые вычисления.
Один из способов оптимизировать производительность приложения — максимально уменьшить количество представлений, выделенных красным цветом.
Изменив label.backgroundColor = UIColor.clear
на label.backgroundColor = UIColor.white
, мы можем уменьшить смешивание слоев между меткой и слоем представления за ней.
Вы могли заметить, что даже если вы установили UIImageView как непрозрачный и присвоили ему цвет фона, симулятор все равно будет отображать красный цвет в представлении изображения. Вероятно, это связано с тем, что изображение, которое вы использовали для просмотра изображения, имеет альфа-канал.
Чтобы удалить альфа-канал для изображения, вы можете использовать приложение «Предварительный просмотр», чтобы сделать дубликат изображения ( Shift ⇧ + Cmd ⌘ + S ), и снять флажок «Альфа» при сохранении.
5. Передача тяжелых функций обработки фоновым потокам (GCD)
Поскольку UIKit работает только в основном потоке, выполнение тяжелой обработки в основном потоке замедлит работу пользовательского интерфейса. Основной поток используется UIKit не только для обработки пользовательского ввода и ответа на него, но и для отрисовки экрана.
Ключом к тому, чтобы сделать приложение отзывчивым, является перемещение как можно большего количества тяжелых задач обработки в фоновые потоки. Избегайте выполнения сложных вычислений, работы в сети и тяжелых операций ввода-вывода (например, чтения и записи на диск) в основном потоке.
Возможно, вы когда-то использовали приложение, которое внезапно перестало реагировать на ваш сенсорный ввод, и вам кажется, что приложение зависло. Скорее всего, это вызвано тем, что приложение выполняет тяжелые вычислительные задачи в основном потоке.
Основной поток обычно чередуется между задачами UIKit (такими как обработка пользовательского ввода) и некоторыми легкими задачами с небольшими интервалами. Если в основном потоке выполняется тяжелая задача, UIKit нужно будет дождаться завершения тяжелой задачи, прежде чем он сможет обрабатывать сенсорный ввод.
По умолчанию код внутри методов жизненного цикла контроллера представления (например, viewDidLoad) и функций IBOutlet выполняется в основном потоке. Чтобы переместить тяжелые задачи обработки в фоновый поток, мы можем использовать очереди Grand Central Dispatch, предоставляемые Apple.
Вот шаблон для переключения очередей:
// Switch to background thread to perform heavy task. DispatchQueue.global(qos: .default).async { // Perform heavy task here. // Switch back to main thread to perform UI-related task. DispatchQueue.main.async { // Update UI. } }
qos
означает «качество обслуживания». Разные значения качества обслуживания указывают на разные приоритеты для указанных задач. Операционная система будет выделять больше процессорного времени и пропускной способности ввода-вывода для задач, размещенных в очередях с более высокими значениями QoS, а это означает, что задача завершится быстрее в очереди с более высокими значениями QoS. Более высокое значение QoS также будет потреблять больше энергии из-за использования большего количества ресурсов.
Вот список значений QoS от самого высокого до самого низкого приоритета:
Apple предоставила удобную таблицу с примерами того, какие значения QoS использовать для разных задач.
Следует иметь в виду, что весь код UIKit всегда должен выполняться в основном потоке. Изменение объектов UIKit (таких как UILabel
и UIImageView
) в фоновом потоке может привести к непредвиденным последствиям, например, фактическое отсутствие обновления пользовательского интерфейса, сбой и т. д.
Из статьи Apple:
«Обновление пользовательского интерфейса в потоке, отличном от основного, является распространенной ошибкой, которая может привести к пропущенным обновлениям пользовательского интерфейса, визуальным дефектам, повреждению данных и сбоям».
Я рекомендую посмотреть видео Apple WWDC 2012 о параллелизме пользовательского интерфейса, чтобы лучше понять, как создавать адаптивные приложения.
Примечания
Компромисс оптимизации производительности заключается в том, что вам нужно писать больше кода или настраивать дополнительные параметры помимо функциональности приложения. Это может привести к тому, что ваше приложение будет доставлено позже, чем ожидалось, и вам придется поддерживать больше кода в будущем, а больше кода означает потенциально больше ошибок.
Прежде чем тратить время на оптимизацию своего приложения, спросите себя, работает ли оно уже гладко или в нем есть какие-то неотзывчивые части, которые действительно нуждаются в оптимизации. Тратить много времени на оптимизацию и без того плавного приложения, чтобы сократить 0,01 секунды, возможно, не стоит, так как время лучше потратить на разработку лучших функций или других приоритетов.
Дополнительные ресурсы
- «A Suite of Delicious iOS Eye Candy», Тим Оливер, Tokyo iOS Meetup 2018 (видео)
- «Создание параллельных пользовательских интерфейсов на iOS», Энди Матушак, WWDC 2012 (видео)
- «Сохранение пользовательского интерфейса вашего приложения при запуске», Apple
- «Руководство по параллельному программированию: очереди отправки», Архив документации, Apple
- «Проверка основного потока», Apple