Введение


Привет, меня зовут Ник Снайдер и я инженер-программист в компании LinkedIn. Сегодня я хочу рассказать вам историю об Auto Layout:

  1. Как мы в LinkedIn используем его.
  2. Проблемы, с которыми мы встретились.
  3. Почему в некоторых ситуациях мы прекратили использовать Auto Layout.
  4. И что мы используем вместо него.

Хорошие новости


Я бы хотел начать с хороших новостей:

  1. Мы в LinkedIn широко используем Auto Layout. Это наш основной метод построения интерфейсов во всех наших приложениях.
  2. Auto Layout мощный инструмент, который облегчает поддержку:
    1. Различных размеров экрана;
    2. Языков с написанием справа налево.

Плохие новости


Плохие новости заключаются в том, что производительность Auto Layout недостаточно хороша:

  1. Не масштабируется для сложных иерархий view (из последующего изложения будет ясно, что речь идет о нелинейном увеличении времени расчета при линейном росте количества view — прим. перев.).
  2. Мы наблюдали регресс производительности, на некоторых релизах iOS.
  3. Производительность может оказаться непредсказуемо плохой для некоторых разметок интерфейса (здесь и далее layout переведено как «разметка» — прим. перев.).

(Далее, где текст стенограммы и текст слайдов значительно дублируют друг друга, я буду осуществлять слияние без потери информации. Текст и слайды использованы с разрешения автора. — прим. перев.)

Проблемы Auto Layout


Рассмотрим пример разметки из приложения LinkedIn. Пусть есть две метки (здесь и далее в тексте используется «labels» — прим. перев.): многострочная метка слева, и однострочная метка справа. Нам нужно, чтобы правая метка имела достаточно места, чтобы отобразить всё своё содержимое. Левая метка содержит просто некоторое количество текста, который должен занять максимум две строки.

Падение производительности Auto Layout
Примечание. Падение производительности Auto Layout. Слева — метка, содержащая максимум две строки. Справа — метка, которая должна обладать следующими свойствами:

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

Для реализации описанной разметки с помощью Auto Layout мы установим для правой метки требуемые свойства: обжатие содержимого и сопротивление сдавливанию. Это отлично работало на iOS 8, и отлично работало на всех тестовых данных в процессе разработки. Но с выпуском iOS 9 эта реализация вызвала огромные проблемы с производительностью у некоторых наших пользователей. И мы не знали об этих проблемах, пока пользователи не стали жаловаться.
image
Примечание. Время выполнения Auto Layout на iPhone 6. Горизонтальная ось — количество view. Вертикальная ось — время выполнения Auto Layout. Синий график — UIScrollView c Auto Layout для двух строк. Красный график — UIScrollView c Auto Layout для одной строки.

Очевидно, что вовсе не здорово получать уведомления от своих пользователей о проблемах с производительностью. Вы можете подумать: «Насколько же плохо оно может быть?». Для данной ситуации, когда метка слева содержит несколько строк, синяя линия показывает, как много времени занимает выполнение разметки, для определенного количества view. Как Вы можете видеть, синяя линяя быстро уходит вверх с ростом числа view. Это и стало источником проблемы.

Такой проблемы нет, если метка слева содержит только одну строку или какой-нибудь другой тип данных. Итак, проблему вызывал конкретный тип данных.

Новостная лента LinkedIn.


  1. С некоторого времени из-за проблем с производительностью Auto Layout не используется.
  2. Каждый view реализует код разметки вручную:
    1. Тяжело поддерживать,
    2. Хотелось бы иметь переиспользуемое решение, чтобы применять на экране Профиля.

В случае с новостной лентой LinkedIn мы на самом деле знали, что производительность Auto Layout отнюдь не превосходна. По этой причине новостная лента LinkedIn долгое время не использовала Auto Layout. В ленте каждый view или ячейка реализуют собственный код разметки, используя layoutSubviews. Такая разметка вручную работает значительно быстрее. Однако проблема в том, что поддержка такого кода выматывает. У нас есть две функции. Первая вычисляет высоту, благодаря чему мы можем сказать таблице или UICollectionView, какой высоты ячейка. А затем вторая выполняет собственно разметку. Причина, по которой мы разделили эту логику в том, что так мы можем выполнить вычисление высоты быстро — без полного выполнения разметки.

Мы хотели нечто подобное для остальных частей приложения. Но мы хотели, чтобы оно подходило для решения разных задач, и чтобы им могли воспользоваться многие.

Решения для разметки


Требования к решению для разметки


  1. Быстрое. Мы хотели, чтобы решение было быстрым наравне с вручную написанным кодом потому, что это — то, что уже было у нас в ленте.
  2. API, который естественен в Swift приложении. Большинство наших приложений в LinkedIn — в том числе основное — написаны на Swift.
  3. Поддерживаемое и используемое в серьезных проектах. Мы бы не хотели использовать программное обеспечение бета-уровня в наших продуктах.
  4. С открытым исходным кодом (отладка, если что-то пойдет не так). Один из источников боли при работе с Auto Layout то, что Auto Layout — это черный ящик. И когда что-то идет не так, у нас нет возможности раскопать причину.
  5. Приемлемая лицензия (с точки зрения юристов компании).

Существующие решения


  1. React Native, AsyncDisplayKit, ComponentKit — у Facebook есть множество хороших библиотек с открытым кодом. К сожалению, из-за лицензии мы не можем использовать их.
  2. Few — библиотека, но выглядит как брошенная — последний коммит более года назад.
  3. Render — создание май 2016. Сейчас мы нашли ещё одну библиотеку: Render. Но она не существовала на момент принятия решения.

Не один из этих проектов не удовлетворил все наши запросы.

Время построить что-то новое...


Итак, не один из найденных в сети проектов не удовлетворял всем нашим требованиям. И мы создали то, что назвали LayoutKit.

LayoutKit — это библиотека для осуществления быстрого позиционирования view на iOS, macOS, tvOS. Далее я расскажу как её применять, и как она работает.

LayoutKit «привет мир»


На верхнем уровне разметка осуществляется в три этапа:

  1. Разработчик определяет разметку используя неизменяемые структуры данных.
  2. LayoutKit вычисляет фреймы для каждого view, причем, по Вашему желанию — в фоновом потоке.
  3. LayoutKit в главном потоке создает все view и назначает им фреймы.

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

Будущая разметка
Первая часть — это создание разметки для 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 (размещение — прим. перев.) является неизменяемой структурой данных, поэтому мы можем передать её обратно в главный поток и сделать вызов 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. Если пропустить «подготовку к анимации» — то ожидаемый результат получить не удастся.

Как работает LayoutKit


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

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

Схема LayoutKit
Примечание. Слева на схеме — множество разметок, по центру — Протокол Разметки, справа — движок разметки.

Также существует движок разметки, который представляет собой набор классов. Эти классы работают опираясь лишь на Протокол Разметки: вычисляя размеры фреймов, создавая их экземпляры (видимо, речь о создании размещений — прим. перев.), исполняя логику анимации.

Базовые разметки


Базовые разметки — это всего лишь вычисления упакованные в Протокол Разметки:

  1. LabelLayout — 124 строки кода,
  2. SizeLayout — 164 строки кода,
  3. InsetLayout — 39 строк кода,
  4. StackLayout — 175 строк кода.

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

Создание собственных разметок


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

  1. Создать нужную разметку используя композицию и вложение четырех базовых.
  2. Создать собственную разметку реализовав Протокол Разметки, который называется «Layout».

Благодаря чему LayoutKit работает быстро?


Две основных причины:

  1. LayoutKit не использует обобщенный алгоритм поиска решения для системы ограничений как Auto Layout. Напротив, каждая разметка использует специализированный алгоритм. Благодаря этому Вы получаете эффективную реализацию для каждой разметки.
  2. Самое медленное, что мы делаем во всём LayoutKit — это сортировка дочерних разметок по гибкости внутри StackLayout. Для всего остального алгоритмическая сложность составляет O(n).

Давайте посмотрим на реально достигнутый уровень производительности в числах. Это пример выполнения на 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. Не настолько быстр, как разметка вручную, но за это Вы получаете много хороших штук без необходимости писать много кода.

Производительность график 1
Примечание. Производительность реализаций разметки. Больше — лучше.

Можно посмотреть на это с другой стороны: сколько времени на исполнение собственного кода у Вас есть в интервале между последовательным отображением двух кадров? Чёрная горизонтальная линия — это 16 миллисекунд. Вы можете видеть, что при использовании UIStackView выполнение разметки для элемента новостной ленты займёт 46 миллисекунд. При использовании Auto Layout — 28 миллисекунд. Что нам говорит этот график — это то, что при использовании Auto Layout или UIStackView, один или два кадра будут пропущены во время каждого выполнения разметки на главном потоке.

LayoutKit и разметка вручную примерно равны. При использовании LayoutKit или разметки вручную требует только 6 миллисекунд. Кроме того с LayoutKit разметка может выполняться в фоновом потоке.

Производительность график 2
Примечание. Время разметки UICollectionView с единственной ячейкой. Меньше — лучше.

Следующее, о чем хочется сказать — это неизменяемые структуры данных.

Неизменяемые структуры данных


Объекты разметок и все промежуточные структуры данных — неизменяемые. Что даёт следующие результаты:

  1. Потокобезопасность. Возможность передавать данные туда и обратно между главным и фоновым потоками.
  2. Можно легко заблаговременно вычислять и кэшировать разметки. Например, можно в фоновом потоке заранее рассчитать разметку для поворота экрана — до того как пользователь действительно повернет экран.
  3. Простая отладка. Если заранее известно, что переменная не может измениться, то и не нужно волноваться о проверке этого.

Другие преимущества LayoutKit


  1. Автоматическая поддержка языков с написанием справа-налево.
  2. Легкость чтения, написания, компоновки и тестирования декларативных разметок.
  3. Быстрое прототипирование в песочнице.
  4. Поддержка iOS, macOS, tvOS.

Применение


Готов ли 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, с которым приятно работать.
Поделиться с друзьями
-->

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


  1. svanichkin
    10.05.2017 10:01

    Насколько я знаю, такие самописные решния использовались не только в LinkedIn, но и в других сервисах, в частности реализовывалась некоторая механика кеширования. Если интересно то об этом можно почитать здесь же на хабре. Но уже с iOS 10 разметка автоматически кешируется, в том числе и для горизонтального режима.


    1. uninova
      10.05.2017 22:26
      +1

      Что значит «кешируется разметка»? Можете линк дать для где подробнее почитать?


  1. Jurasic
    16.05.2017 00:24

    React Native, AsyncDisplayKit, ComponentKit — у Facebook есть множество хороших библиотек с открытым кодом. К сожалению, из-за лицензии мы не можем использовать их.


    Интересно почему им BSD 3-clause лицензия им не подошла?


  1. IrixV
    18.05.2017 21:20

    К сожалению, ни UICollectionView, ни тем более UITableView в базовом исполнении не дают необходимой производительности, даже если не использовать AutoLayout. Столкнулась с этим, когда делала приложение для автомобильного форума. Что делает UITableView если в несколько ячеек подряд затолкать по 50 фотографий в перемешку с разноформатным текстом — очень сильно дергает экран, а в худшем случае приложение падает. Но, спасибо FB за его бесценные библиотеки.


    1. t-nick
      18.05.2017 23:39

      Тут еще нужно обратить внимание на особенность работы UIImage и UIImageView. У первого есть внутренний кеш, но при первом рендеринге он конечно же пуст, и загрузка изображения в память происходит в главном потоке, что добавляет тормозов даже для небольших изображений на современных устройствах. Выход — предварительный рендеринг UIImage в битмап контексте на фоновом потоке для заполнения кеша.