Каждый iOS-разработчик помнит радость, когда увидел свое первое приложение, работающим на iOS-устройстве. Интерфейс iPhone, ориентированный на человека, делает программы живыми. Когда вы выбираете iOS-разработку в качестве карьеры, эта радость возрастает по мере того, как ваше приложение затрагивает жизни все большего числа людей.
Влияние на большее количество пользователей часто связано с новыми функциями, потоками и функциями iOS. Но по мере того, как приложение растет для обслуживания большего числа людей, новые функции и функционал могут вносить дополнительный вес и сложность, что замедляет итерации продукта и исключает атомарные изменения.
Мы, как команда iOS в Airbnb, прошли этот путь. Мы очень рады осознавать, что код, который мы поставляем каждую неделю, обеспечивает незабываемый отпуск для гостей и новые источники доходов для принимающих их предпринимателей. Внимание к дизайну заложено в нашей ДНК, и мы безмерно гордимся совершенствованием каждой детали того, что мы показываем нашим пользователям. В то же время мы не застрахованы от проблем, связанных с масштабной разработкой.
В этой статье мы рассмотрим трудности, с которыми мы столкнулись при удовлетворении растущих бизнес-потребностей Airbnb. Мы расскажем, как инвестиции в технологии, владение кодом и процессы позволили iOS-команде Airbnb получить лучшее из обоих миров: работать с кодовой базой, влияющей на весь мир, не чувствуя ее веса.
Проблемы разработки масштабного iOS-приложения
Стажер Airbnb совершил первый коммит в наше приложение для iOS 16 июня 2010 года. С тех пор этот проект Xcode превратился в базу с 1.5 миллионами строк собственного кода. Сегодня над нашим приложением работают около 75 iOS-инженеров. Мы выпускаем приложение еженедельно на 62 языках, поддерживая сообщество гостей и хозяев почти во всех странах на планете.
В масштабе Airbnb организация кода стала проблемой. Мы приветствуем и поощряем эксперименты и новые идеи. Когда мы в достаточной мере изучим пространство решений, мы оцениваем его “с точки зрения Airbnb” (Airbnb way). До недавнего времени большая часть нашего кода была организована в модули в плоском каталоге lib/. Без какой-либо иерархии или категоризации нашего кода инженерам стало трудно находить существующие реализации универсальных возможностей. Мы начали замечать в нашем коде множество способов выполнить одну и туже задачу, что привело к раздуванию двоичного кода, который мы отправляли пользователям. Более того, мы обнаружили, что каждая конкурирующая реализация одной и той же возможности, как правило, была более низкого качества, чем одна реализация, которая привлекала больше инвестиций и внимания.
Хотя Xcode — это инструмент, с которым инженеры iOS чувствуют себя наиболее комфортно, мы обнаружили, что Xcode невозможно изящно масштабировать для кодовой базы нашего размера и сложности. Мало того, что файлы проекта Xcode сложно проверять в пул реквестах, но и частота конфликтов слияния и состояний гонки в этих файлах проекта увеличивалась по мере того, как большая группа инженеров двигалась в разработке с высокой скоростью. Даже открытие Xcode может превратиться в проблему с большой кодовой базой. Более года назад мы замерили, что Xcode требуется от одной до двух минут, чтобы стать интерактивным при загрузке рабочего пространства со всем нашим исходным кодом.
Большинство iOS-инженеров были разочарованы длительным временем сборки и медленными циклами итераций. В какой-то момент особенно креативный инженер обнаружил, что его ноутбук компилирует код быстрее, если он отключит свой внешний монитор. Многие инженеры приучили себя подключать зарядный кабель USB-C к правой стороне своего MacBook Pro, чтобы избежать потери производительности из-за теплового троттлинга. Когда кодовая база Airbnb составляла менее 500 тыс. строк собственного кода, некоторые из этих проблем можно было исправить с помощью более мощного оборудования, хотя мы также определили практический предел этого решения. Стало трудно чувствовать, что вы делаете лучшую работу, когда большую часть дня вы тратили на ожидание завершения сборки.
Эти вызовы росли органически. Отзывы новых сотрудников предоставили ценный взгляд на то, как нам следует расставлять приоритеты в потребностях инфраструктуры, поскольку iOS-инженеры, которые пришли из компаний с небольшими проектами, еще не привыкли к медлительности и обходным путям чрезмерно разросшейся кодовой базы. Мы знали, что нужно что-то изменить, чтобы Airbnb продолжал выпускать iOS-приложения мирового класса.
Решения, которые мы внедрили
На протяжении многих лет мы исследовали и внедрили множество решений для решения указанных выше проблем. В этом посте мы обсудим три самых ценных подхода, которые позволили нам эффективно работать в большом масштабе. Мы думаем, что эти высокоуровневые темы будут применимы и к другим малым и средним iOS-командам, которые быстро растут.
Внедрение современной системы сборки
Xcode остается предпочтительной IDE для iOS-инженеров в Airbnb. В то же время мы видели в других систем сборки функции, которые, как мы знали, могут повысить продуктивность iOS-разработчиков. Некоторые из них стоит выделить: сетевые кэши артефактов сборки, интерфейс запросов для графа сборки и простой способ добавления кастомных шагов в виде зависимостей. Мы считаем, что эти возможности — залог успеха современной системы сборки.
Этим требованиям соответствовала система сборки Buck от Facebook. Мы начали серьезно обсуждать Buck в конце 2016 года и всерьез приступили к исследованиям в 2017 году. В 2019 году мы полностью перешли на декларативную систему сборки Buck. Мы получили большую пользу от Buck, хотя и обнаружили, что общедоступная документация оставляет желать лучшего. Соответственно, мы разместили нашу настройку Buck в общедоступном репозитории GitHub.
В рамках этого перехода мы удалили наши проекты Xcode, управляемые вручную, в пользу декларативных файлов BUCK, которые живут рядом с кодом каждого модуля. Файлы BUCK определяются с использованием языка Starlark, который совместим с Bazel, другой популярной современной системой сборки. Ниже представлена структура существующего модуля Airbnb.
Наши инфраструктурные команды придерживаются подхода, согласно которому мы должны работать с инженерами там, где они находятся, и при этом улучшать их опыт разработки. Соответственно, iOS-инженеры продолжают разработку в рабочем пространстве Xcode, которое создается из графа сборки, определенного в Buck.
Изначально только наши CLI сборки Buck для iOS-приложения Airbnb могли получить выгоду от HTTP-кеша Buck. Одно это уже было большим улучшением, поскольку позволило нам убедиться, что сборка для App Store будет успешной при каждом пул реквесте, не замедляя работу инженеров. Однако локальные сборки Xcode не могли извлекать артефакты из кеша, поскольку сгенерированное рабочее пространство Xcode продолжало использовать стандартную систему сборки Xcode.
Мы продолжили использовать современную систему сборки в основе нашего приложения, чтобы сократить цикл итераций для инженеров. Мы сделали возможным создание рабочего пространства Xcode, которое внутренне собирает приложение с помощью системы сборки Buck. Все стандартные инструменты Xcode (точки останова, консоль, ошибки), которые iOS-инженеры используют каждый день, работают должным образом.
Рабочее пространство Xcode на основе Buck улучшило скорость локальной сборки, поскольку оно может использовать кэш Buck. Чтобы сократить время запуска Xcode, мы также сделали возможным создание рабочего пространства Xcode из подмножества всей нашей кодовой базы. Все приложение по-прежнему строится с помощью Buck, а Xcode становится интерактивным в кратчайшие сроки.
Проектирование типов модулей
Чтобы решить проблему отсутствия иерархии в нашем коде и, как следствие, невозможности обнаружения требуемого кода, мы разработали организационную структуру. Модули организованы в семантически значимые группы, называемые типами модулей (module types).
Мы написали точную документацию для наших типов модулей. Поскольку концепция типов модулей является фундаментальной для того, как разработчики iOS работают в Airbnb, эта документация размещена на нашем внутреннем портале для разработчиков и управляется системой контроля версий. Мы резюмируем каждый тип модуля всего в нескольких абзацах, объясняя назначение типа модуля и типы кода, для поддержки которого он был разработан.
При разработке этой архитектуры мы учитывали как прикладное программирование, так и передовые методы построения систем. Каждый тип модуля имеет строгий набор правил видимости. Эти правила видимости определяют допустимые зависимости между модулями этого типа. Каждый отдельный модуль может повысить свою заметность, эта техника используется некоторыми более крупными командами, которые пользуются преимуществами модульности и хотят избежать неожиданных входящих зависимостей от своих модулей. Отдельный модуль не может расширить свою видимость за пределы, налагаемые его типом модуля.
Давайте посмотрим на примере…
Фича — это один из наших основных типов модулей. На Airbnb фичи доступны пользователям. В нашем коде iOS адресатом, обращенным к пользователю, является UIViewController, который будет представлен модально или установлен в UINavigationController. Функциональный модуль должен быть привязан к одному месту назначения, обращенному к пользователю, когда это возможно, хотя он может содержать несколько UIViewController, которые реализуют назначение.
Фиче модули не видны другим модулям (т.е. модуль не может зависеть от модуля); однако они совместно используют облегченные типы через родственный тип модуля, называемый интерфейсом фичи.
Каждая фича имеет соответствующий интерфейс фичи, который имеет более широкую видимость. Фича может зависеть от любого количества интерфейсных модулей и всегда зависит от своего собственного интерфейсного модуля. Интерфейс функционирует аналогично тому, как файлы заголовков работают в программах Clang.
Правила видимости для типа фиче модуля гарантируют, что все функциональные модули независимы друг от друга. Тип интерфейсного модуля позволяет фичам совместно использовать простые типы (протоколы, перечисления, типы значений), обеспечивая такие возможности, как строго типизированная маршрутизация между фичами.
В дополнение к типу фиче модуля, тип сервисного модуля является домом для объектов, не относящихся к пользовательскому интерфейсу, которые отвечают за управление состоянием, которое совместно используется фичами. Любой сервисный модуль может дополнительно иметь интерфейсный родственный модуль.
Сейчас у нас в iOS Airbnb двенадцать типов модулей.
Наши семантически значимые типы модулей действуют как оглавление для нашей очень большой кодовой базы. Инженеры сразу получают достаточно точную мысленную модель модуля в зависимости от его типа. 90% нашего собственного кода было перенесено с lib/ на модули. Отличный доклад моего коллеги Франциско более подробно описывает, как наша стратегия организации кода эволюционировала от папок к типам модулей, а также проливает свет на то, как мы реализовали эту крупную миграцию.
Создание Dev Apps
Наши инвестиции в системы сборки и архитектуру iOS-приложений сделали возможным третье нововведение — Dev App. Dev App — это временное рабочее пространство Xcode по запросу для отдельного модуля и его зависимостей.
Разрабатываемые приложения (Dev Apps) возникли в Android экосистеме Airbnb. Популярность и успех как Android, так и iOS Dev Apps проистекают из простой аксиомы: сведение к минимуму скоупа IDE до файлов, которые вы редактируете, уменьшает цикл разработки. Когда в вашем рабочем пространстве Xcode меньше кода, Xcode может быстрее индексировать и компилировать этот код.
Внедрение типов модулей в нашу кодовую базу разрушило дорогостоящие зависимости между функциональными модулями. Теперь у модулей минимальные зависимости. Например, сборка любого функционального модуля и всех его зависимостей всегда намного дешевле, чем сборка всего приложения Airbnb. Поскольку функциональные модули не могут зависеть от других функциональных модулей, мы определили возможность мегафункций, которые транзитивно создают все приложение.
Инженеры iOS создают Dev Apps с помощью надежного и удобного интерфейса командной строки. Команда для создания Dev App следует передовым практикам Unix с упором на то, чтобы быть доступной для инженеров, которым может быть неудобно работать с Терминалом. Под капотом инструмент использует интерфейс запросов Buck для составления полного списка исходных файлов.
Инструмент командной строки Dev App генерирует контейнерное приложение iOS для размещения функции и открывает сгенерированное рабочее пространство Xcode. Разработчики определяют варианты своей фичи в непроизводственном коде. Эти варианты позволяют одним касанием получить доступ к любому возможному состоянию пользовательского интерфейса. Контейнерное приложение Dev App обеспечивает удобство для обычных рабочих процессов, таких как, например, присоединение токена OAuth к HTTP-запросам.
Dev App позволяет разработчику продукта повторять пользовательский интерфейс своей функции и большую часть его бизнес-логики, работая при этом лишь с частью всего приложения Airbnb. Хотя Dev App были разработаны для фич и модулей пользовательского интерфейса, теперь мы поддерживаем создание Dev App для любого типа модулей. Мы обнаружили, что многие разработчики iOS также предпочитают работать с модулями, не относящимися к пользовательскому интерфейсу, в этой минимальной среде Xcode.
По-прежнему критически важно создавать и запускать все приложение Airbnb во многих ситуациях, особенно для тестирования взаимодействия функций друг с другом. Однако, когда вы работаете над кодом, который достаточно изолирован и хорошо протестирован, можно уверенно сделать это изменение с помощью одного только Dev App.
Прорыв с другой стороны
Наши усилия создали экосистему, в которой команды могут работать независимо на своей цели. Программирование в Dev App напоминает о радостях программирования в более простом и небольшом проекте со всеми преимуществами поддержки хорошими сторонними инструментами и фреймворками.
Dev App теперь используются в более чем 50% локальных сборок. 75 процентиль времени сборки Dev App составляет менее двух минут, а 50 процентиль — менее одной минуты. Мы заставляем Dev Apps максимально использовать фиктивные зависимости, чтобы сократить объем кода, который необходимо создать.
Архитектура нашего приложения открыла эру 100% владения кодом. Мы сохранили 100% владение за счет реорганизации команды отчасти благодаря нашей высокомодульной кодовой базе, которая позволяет передавать и перераспределять право собственности с минимальным рефакторингом. Сегодня наш собственный код разделен почти на 1500 модулей.
У нас более 50% тестового покрытия для кода в нашей современной модульной структуре, в то время как код, который остается в устаревшей модульной структуре, имеет 23% покрытие. Частично это связано с интерфейсными модулями, которые заставляют разработчиков писать сервисы с использованием протокол-ориентированного программирования . Когда код взаимодействует в основном с протоколами, легко создавать тестовые двойники для зависимостей.
Сохраняя строгие правила видимости между типами модулей, мы достигли высоко распараллеленного графа сборки. Изучая трассировку сборки полного приложения Airbnb на 8-ядерном 16-дюймовом MacBook Pro 2019 года, мы видим полное использование доступных ресурсов ЦП почти в 80% фазы компиляции.
Наша современная система сборки позволила создать здоровую экосистему инструментов командной строки, которые значительно упрощают общие задачи. Создание модуля ранее включало выполнение длинного контрольного списка шагов, подверженных ошибкам. Теперь инженеры создают модули с помощью интерактивной команды Rake. Мы даже использовали наш интерфейс запросов Buck для создания инструмента командной строки, который направляет инженеров через шаги, необходимые для миграции их библиотек/модулей в новую структуру.
И последнее, но не менее важное: больше не передавая проекты Xcode в систему управления версиями, мы легко может добавлять и удалять зависимости модулей с простым способом разрешения любых конфликтов слияния.
Запечатываем конверт
В Airbnb мы с энтузиазмом в целом относимся к развитию индустрии iOS-разработки. Мы верим в силу нативных приложений, работающих на мобильных устройствах, и хотим, чтобы другие компании использовали проделанную нами работу, чтобы они могли сосредоточить больше энергии на создании впечатлений, которые нравятся пользователям.
Мы знаем, что мы не единственная компания, чье приложение органически выросло из небольшого проекта до чего-то большего, чем предполагалось первоначальным автором. Мы знаем, что многие из наших компаний-аналогов претерпели преобразования, аналогичные нашей. Каждое из вышеупомянутых решений зависили от наших обстоятельств и культуры, хотя мы видим, что эти темы всегда актуальны. Мы рады продолжить эту работу в открытом доступе с нашими коллегами в рамках Mobile Native Foundation.
Наш путь к повышению производительности еще не завершен. Заглядывая в будущее, мы видим огромную возможность использовать статический анализ Swift для генерации стандартного кода и повышения переносимости кода функций и сервисов. Мы продолжим улучшать цикл сборки/тестирования/запуска при разработке iOS-продукта, чтобы вес мегаприложения не заглушал радость инди-разработки. И мы жаждем современной системы сборки, одобренной Apple.
Мы считаем, что только прикоснулись к мобильным вычислениям. Мы продолжим совершенствовать инструменты, технологии и человеческие процессы, необходимые для масштабных инноваций.