- PVSM.RU - https://www.pvsm.ru -
Привет, меня зовут Ник Снайдер и я инженер-программист в компании LinkedIn. Сегодня я хочу рассказать вам историю об Auto Layout:
Я бы хотел начать с хороших новостей:
Плохие новости заключаются в том, что производительность Auto Layout недостаточно хороша:
(Далее, где текст стенограммы и текст слайдов значительно дублируют друг друга, я буду осуществлять слияние без потери информации. Текст и слайды использованы с разрешения автора. — прим. перев.)
Рассмотрим пример разметки из приложения LinkedIn. Пусть есть две метки (здесь и далее в тексте используется «labels» — прим. перев.): многострочная метка слева, и однострочная метка справа. Нам нужно, чтобы правая метка имела достаточно места, чтобы отобразить всё своё содержимое. Левая метка содержит просто некоторое количество текста, который должен занять максимум две строки.
Примечание. Падение производительности Auto Layout. Слева — метка, содержащая максимум две строки. Справа — метка, которая должна обладать следующими свойствами:
Для реализации описанной разметки с помощью Auto Layout мы установим для правой метки требуемые свойства: обжатие содержимого и сопротивление сдавливанию. Это отлично работало на iOS 8, и отлично работало на всех тестовых данных в процессе разработки. Но с выпуском iOS 9 эта реализация вызвала огромные проблемы с производительностью у некоторых наших пользователей. И мы не знали об этих проблемах, пока пользователи не стали жаловаться.
Примечание. Время выполнения Auto Layout на iPhone 6. Горизонтальная ось — количество view. Вертикальная ось — время выполнения Auto Layout. Синий график — UIScrollView c Auto Layout для двух строк. Красный график — UIScrollView c Auto Layout для одной строки.
Очевидно, что вовсе не здорово получать уведомления от своих пользователей о проблемах с производительностью. Вы можете подумать: «Насколько же плохо оно может быть?». Для данной ситуации, когда метка слева содержит несколько строк, синяя линия показывает, как много времени занимает выполнение разметки, для определенного количества view. Как Вы можете видеть, синяя линяя быстро уходит вверх с ростом числа view. Это и стало источником проблемы.
Такой проблемы нет, если метка слева содержит только одну строку или какой-нибудь другой тип данных. Итак, проблему вызывал конкретный тип данных.
В случае с новостной лентой LinkedIn мы на самом деле знали, что производительность Auto Layout отнюдь не превосходна. По этой причине новостная лента LinkedIn долгое время не использовала Auto Layout. В ленте каждый view или ячейка реализуют собственный код разметки, используя layoutSubviews. Такая разметка вручную работает значительно быстрее. Однако проблема в том, что поддержка такого кода выматывает. У нас есть две функции. Первая вычисляет высоту, благодаря чему мы можем сказать таблице или UICollectionView, какой высоты ячейка. А затем вторая выполняет собственно разметку. Причина, по которой мы разделили эту логику в том, что так мы можем выполнить вычисление высоты быстро — без полного выполнения разметки.
Мы хотели нечто подобное для остальных частей приложения. Но мы хотели, чтобы оно подходило для решения разных задач, и чтобы им могли воспользоваться многие.
Не один из этих проектов не удовлетворил все наши запросы.
Итак, не один из найденных в сети проектов не удовлетворял всем нашим требованиям. И мы создали то, что назвали LayoutKit.
LayoutKit — это библиотека для осуществления быстрого позиционирования view на iOS, macOS, tvOS. Далее я расскажу как её применять, и как она работает.
На верхнем уровне разметка осуществляется в три этапа:
Для лучшего понимания рассмотрим пример, с простой, но полностью реализованной разметкой. В этой разметке будет и мир, и изображение, и текстовая метка.
Первая часть — это создание разметки для UIImageView. Итак, нам нужна разметка фиксированного размера, которая у нас называется SizeLayout содержащая UIImageView, и с шириной и высотой равными 50 пикселей (прим. перев. — здесь и далее в оригинале pixels). В блоке конфигурации мы устанавливаем изображение для UIImageView:
let image = SizeLayout<UIImageView>(
width: 50,
height: 50,
config: { imageView in
imageView.image = UIImage(named: “earth.jpg”)
}
)
Далее нам нужна разметка для метки. Мы устанавливаем текст и выравнивание по центру доступного пространства.
let label = LabelLayout(
text: “Hello World!”,
alignment: .center
)
Мы хотим расположить эти view рядом друг с другом, поэтому создадим горизонтальный стэк с интервалом 4 пикселя.
StackLayout(
axis: .horizontal,
spacing: 4,
sublayouts: [image, label]
)
В завершение нам нужны отступы по краям. Мы создаем InsetLayout, который оборачивает только что созданный StackLayout.
helloWorld = InsetLayout(
insets: UIEdgeInsets(top: 4, left: 4,
bottom: 4, right: 8),
sublayout: stack
)
Наша «привет мир»-разметка готова, и мы вызываем метод размещения. Метод рекурсивно вычисляет все фреймы для всех view и разметок. Это может выполняться в фоновом потоке.
С момента своего создания arrangement (размещение [1] — прим. перев.) является неизменяемой структурой данных, поэтому мы можем передать её обратно в главный поток и сделать вызов makeViews.
// Можно выполнять в фоновом потоке.
let arrangement = helloWorld.arrangement()
// Должно выполняться в главном потоке.
arrangement.makeViews(in: rootView)
Мы передаем в метод makeViews параметр rootView, чтобы нужные view были созданы сразу в нём. Если параметр не передавать, makeViews вернет view, с которым мы можем делать всё, что нам заблагорассудится.
Вот мы и закончили разметку.
В примере рассмотренном выше мы вызывали arrangement без параметров:
// Размеры определяются содержимым.
// Ограничения не установлены.
helloWorld.arrangement()
Сделаем по другому:
// Явное ограничение по ширине.
helloWorld.arrangement(width: 200)
Вы можете задать явную ширину, и метод выполнит по ней разметку:
Можно видеть, что отступы увеличились, теперь занято всё доступное в ширину пространство. Эта ширина может быть, например, шириной экрана.
Разметки можно анимировать. Мы сделаем это на примере простого SizeLayout. Назовём его «box». Используя параметр viewReuseId, можно задать для view или разметки уникальный идентификатор. Благодаря этому LayoutKit знает какой view в состоянии «до» соответствует какому view в состоянии «после».
В этом примере в состоянии «до» наш «box» — просто квадрат 50х50 пикселей, а в состоянии после — квадрат 25х25 пикселей.
// Задаем анимируемым разметкам параметр viewReuseId
// Состояние "до"
let before = SizeLayout(
width: 50, height: 50, viewReuseId: “box”)
// Состояние "после"
let after = SizeLayout(
width: 25, height: 25, viewReuseId: “box”)
Это две разных разметки. Создадим размещение view с помощью разметки «до». Созданные из этого размещения view поместим в некоторый корневой view — rootView. Затем с помощью разметки «после» подготовимся к анимации — создадим специальный объект для анимации, у которого есть метод animate.
// Исходная разметка "до" и создание view на её основе в корневом rootView.
before.arrangement().makeViews(in: rootView)
// Подготовка к анимации.
let animation = after.arrangement()
.prepareAnimation(for: rootView)
После получения объекта animation можно применить обычный вызов UIView.animate, withDuration, передав ему метод animation.apply, который и осуществляет анимацию смены одной разметки на другую.
// Запуск анимации.
UIView.animate(withDuration: 5.0,
animations: animation.apply)
(к сожалению, анимацию не видно даже на видео в источнике — прим. перев.)
Здесь есть красный и два серых квадрата. Единственное замечание — красный квадрат в начале является дочерним view верхнего квадрата, а затем становится дочерним view нижнего. Оба квадрата двигаются слева направо. Нижний квадрат ужимается в размерах, а красный — растет.
Этап «подготовки к анимации» — это то, что позволяет делать такие сложные анимации со сменой родительского view. Если пропустить «подготовку к анимации» — то ожидаемый результат получить не удастся.
Хотелось бы сказать о преимуществах Swift. LayoutKit написан на Swift, благодаря возможностям которого удалось обеспечить чистый API. Мы используем дженерики, расширения протоколов, и параметры по умолчанию в инициализаторе. В рассмотренных выше примерах Вы уже могли видеть полученные выгоды.
Как же работает LayoutKit? В центре всего LayoutKit располагается Протокол Разметки, и он определяет что есть разметка. Кто угодно может реализовать данный протокол, и LayoutKit предоставляет небольшой набор предустановленных (базовых) разметок, которые его реализуют.
Примечание. Слева на схеме — множество разметок, по центру — Протокол Разметки, справа — движок разметки.
Также существует движок разметки, который представляет собой набор классов. Эти классы работают опираясь лишь на Протокол Разметки: вычисляя размеры фреймов, создавая их экземпляры (видимо, речь о создании размещений [1] — прим. перев.), исполняя логику анимации.
Базовые разметки — это всего лишь вычисления упакованные в Протокол Разметки:
Все эти разметки мы уже видели в примерах. Они являются основными блоками, и они дают возможность построения приличного количества разных пользовательских интерфейсов.
Если 4 базовых разметок недостаточно, чтобы описать Ваш пользовательский интерфейс, то можно сделать следующее:
Две основных причины:
Давайте посмотрим на реально достигнутый уровень производительности в числах. Это пример выполнения на iPhone 6 с iOS 9. Использован UICollectionView с UICollectionViewFlowLayout, содержащий 20 ячеек. Каждая ячейка в целом напоминает свой аналог из новостной ленты LinkedIn. Это занимает довольно много места на экране.
На данном графике «больше» — «лучше». Auto Layout мы обозначили как точку отсчета 1х. Можно видеть, что если Вы примените UIStackView, то это будет работать медленнее, чем Auto Layout. Это потому, что UIStackView построен на базе Auto Layout. Крайним справа изображен результат применения разметки вручную. Разметка вручную в 9,4 раза быстрее, чем Auto Layout. Зеленый столбик — это LayoutKit, и он в 7,7 раза быстрее, чем Auto Layout. Не настолько быстр, как разметка вручную, но за это Вы получаете много хороших штук без необходимости писать много кода.
Примечание. Производительность реализаций разметки. Больше — лучше.
Можно посмотреть на это с другой стороны: сколько времени на исполнение собственного кода у Вас есть в интервале между последовательным отображением двух кадров? Чёрная горизонтальная линия — это 16 миллисекунд. Вы можете видеть, что при использовании UIStackView выполнение разметки для элемента новостной ленты займёт 46 миллисекунд. При использовании Auto Layout — 28 миллисекунд. Что нам говорит этот график — это то, что при использовании Auto Layout или UIStackView, один или два кадра будут пропущены во время каждого выполнения разметки на главном потоке.
LayoutKit и разметка вручную примерно равны. При использовании LayoutKit или разметки вручную требует только 6 миллисекунд. Кроме того с LayoutKit разметка может выполняться в фоновом потоке.
Примечание. Время разметки UICollectionView с единственной ячейкой. Меньше — лучше.
Следующее, о чем хочется сказать — это неизменяемые структуры данных.
Объекты разметок и все промежуточные структуры данных — неизменяемые. Что даёт следующие результаты:
Готов ли LayoutKit к использованию в индустрии? Да, мы используем его в основном приложении LinkedIn, а также в приложении для поиска работы.
По нашему опыту, было весьма несложно обучить инженеров LayoutKit. С другой стороны, обучение Auto Layout таким простым не было.
1) LayoutKit — открытое ПО. Код можно получить на layoutkit.org
2) Лицензия Apache 2.0, поэтому никаких патентных махинаций.
3) Дата релиза 22 июня, 2016 года.
4) Сегодня (я взял актуальные данные на начало мая 2017 — прим. перев.) на гитхабе: 59 наблюдателей, 1996 звезд, 136 форков.
Спасибо всем, кто помогал в работе над LayoutKit. Спасибо, Sergei Taguer (Сергей Тагер), Andy Clark (Энди Кларк), Peter Livesey (Питер Ливси).
Ник Снайдер — инженер по разработке программного обеспечения в LinkedIn. В настоящее время работает над построением инфраструктуры для мобильных приложений масштабируемой на все приложения компании. Участвовал в подготовке трех приложений компании, включая последнюю версию основного приложения. Любимое дело — создание переиспользуемых компонентов с чистым API, с которым приятно работать.
Автор: Артем
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/performance/254797
Ссылки в тексте:
[1] размещение: https://github.com/linkedin/LayoutKit/blob/master/Sources/LayoutArrangement.swift
[2] Источник: https://habrahabr.ru/post/328242/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.