Site icon AppTractor

Эволюция архитектуры приложения Facebook для iOS

Meta, в том числе ее продукты Facebook и Instagram, признана экстремистской организацией в России.

Facebook для iOS (FBiOS) — старейшая мобильная кодовая база в Meta. С тех пор как приложение было переписано в 2012 году, над ним работали тысячи инженеров, оно было отправлено миллиардам пользователей, и оно может поддерживать итерации сотен инженеров одновременно.

После многих лет изменений кодовая база приложения Facebook не похожа на типичную кодовую базу iOS:

FBiOS никогда не проектировалась таким образом намеренно. Кодовая база приложения отражает 10-летнюю эволюцию, вызванную техническими решениями, необходимыми для поддержки растущего числа инженеров, работающих над приложением, его стабильностью и, прежде всего, всем пользовательским опытом.

Теперь, чтобы отпраздновать 10-летний юбилей кодовой базы, мы проливаем свет на технические решения, лежащие в основе этой эволюции, а также на их исторический контекст.

2014: Создание собственного мобильного фреймворка

Через два года после того, как Meta запустила нативную версию приложения Facebook, кодовая база новостной ленты (News Feed) начала испытывать проблемы с надежностью. В то время модели данных новостной ленты опирались на дефолтный фреймворк Apple для управления моделями данных — Core Data. Объекты в Core Data изменяемы (mutable), и это не подходило для многопоточной архитектуры новостной ленты. Что еще хуже, News Feed использовал двунаправленный поток данных, основанный на использовании шаблона проектирования Apple для Cocoa-приложений — Model View Controller.

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

При рассмотрении новых дизайнов один инженер исследовал React, UI-фреймворк Facebook с открытым исходным кодом, который стал довольно популярным в JavaScript-сообществе. Декларативный дизайн React абстрагировался от сложного императивного кода, который вызывал проблемы в фиде (в вебе), и использовал односторонний поток данных, что значительно упростило анализ кода. Эти характеристики казались хорошо подходящими для проблем, с которыми столкнулся News Feed. Была только одна проблема.

В SDK Apple не было декларативного пользовательского интерфейса.

Swift еще не будет анонсирован в течение нескольких месяцев, а SwiftUI (декларативный UI-фреймворк Apple) не будет анонсирован до 2019 года. Если News Feed захочет иметь декларативный UI, команде придется создать новый UI-фреймворк.

В конце концов, это то, что они и сделали.

После нескольких месяцев, потраченных на создание и перенос новостной ленты на новый декларативный UI и новую модель данных, производительность FBiOS увеличилась на 50%. Несколько месяцев спустя разработчики открыли исходный код своего вдохновленного React UI-фреймворка для мобильных устройств — ComponentKit.

По сей день ComponentKit по-прежнему является фактическим выбором для создания нативных пользовательских интерфейсов в Facebook. Он обеспечил бесчисленные улучшения производительности за счет пулов повторного использования представлений, упрощения представлений и просчета макетов в фоне. Он также вдохновил создание Android-аналога, Litho, и самого SwiftUI.

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

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

2015: переломный момент в архитектуре

К 2015 году Meta поставила все на свою мантру Mobile First, а кодовая база FBiOS продемонстрировала стремительный рост числа ежедневных участников. По мере того, как все больше и больше продуктов интегрировалось в приложение, время его запуска стало ухудшаться, и люди начали это замечать. К концу 2015 года скорость запуска приложения была настолько низкой (почти 30 секунд!), что оно могло быть закрыто операционной системой телефона.

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

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

2016: Dylibs и модульность

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

В то время как «pre-main» операции вносил лишь небольшую часть в те 30 секунд, что тратились на запуск, это вызывало особую озабоченность, поскольку время продолжало расти с неограниченной скоростью по мере того, как FBiOS продолжала накапливать новые функции.

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

Изначально структура dylib FBiOS выглядела так:

Были созданы две продуктовые dylib (FBCamera и NotOnStartup), а третья dylib (FBShared) использовалась для совместного использования кода между различными dylib и двоичным файлом основного приложения.

Решение dylib работало прекрасно. FBiOS смогла сдержать неограниченный рост времени запуска приложения. Шли годы, большая часть кода оказывалась в dylib, так что производительность при запуске оставалась высокой и не зависела от постоянных колебаний добавленных или удаленных продуктов в приложении.

Добавление dylib вызвало ментальный сдвиг в том, как инженеры по продуктам Meta писали код. С добавлением dylib, рантайм API-интерфейсы, такие как NSClassFromString(), подвергались риску сбоев во время выполнения, поскольку требуемый класс находился в еще незагруженных dylib. Поскольку многие базовые абстракции FBiOS были построены на итерации всех классов в памяти, FBiOS пришлось переосмыслить, как работают многие из ее основных систем.

Помимо сбоев во время выполнения, dylib также представили новый класс ошибок компоновщика. Если код в Facebook (запускаемый на старте) ссылается на код в dylib, инженеры увидят такую ошибку компоновщика:

Undefined symbols for architecture arm64:
   "_OBJC_CLASS_$_SomeClass", referenced from:
      objc-class-ref in libFBSomeLibrary-9032370.a(FBSomeFile.mm.o)

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

Внезапно:

int main() {
   DoSomething(context);
}

Превращалось в:

int main() {
   FBCallFunctionInDylib(
      NotOnStatupFramework,
      DoSomething,
      context
   );
}

Решение сработало, но довольно плохо пахло:

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

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

2017: Переосмысление архитектуры FBiOS

С введением dylib пришлось переосмыслить несколько ключевых компонентов FBiOS:

Чтобы решить эти проблемы, FBiOS обратилась к системе сборки Meta с открытым исходным кодом Buck.

Внутри Buck каждая «цель» (приложение, dylib, библиотека и т.д.) объявляется с некоторой конфигурацией, например так:

apple_binary(
   name = "Facebook",
   ...
   deps = [
      ":NotOnStartup#shared",
      ":FBCamera#shared",
   ],
)

apple_library(
   name = "NotOnStartup",
   srcs = [
      "SomeFile.mm",
   ],
   labels = ["special_label"],
   deps = [
      ":PokesModule",
      ...
   ],
)

Каждая «цель» содержит всю информацию, необходимую для ее сборки (зависимости, флаги компилятора, исходники и т. д.), и при вызове «buck build» вся эта информация выстраивается в граф, который можно запрашивать.

$ buck query “deps(:Facebook)”
> :NotOnStartup
> :FBCamera

$ buck query “attrfilter(labels, special_label, deps(:Facebook))”
> :NotOnStartup

Используя эту базовую концепцию (и некоторый особый ингредиент), FBiOS начало создавать некоторые запросы к buck, которые могли генерировать целостное представление о классах и функциях в приложении во время сборки. Эта информация станет строительным блоком архитектуры следующего поколения приложения.

2018: Быстрое увеличение сгенерированного кода

Теперь, когда FBiOS смогла использовать Buck для запроса информации о коде в зависимости, она могла создать сопоставление «функции/классы -> dylibs», которое можно было генерировать на лету.

{
   "functions": {
      "DoSomething": Dylib.NotOnStartup,
      ...
   },
   "classes": {
      "FBSomeClass": Dylib.SomeOtherOne
   }
}

Используя это сопоставление в качестве входных данных, FBiOS использовала его для генерации кода, абстрагирующего enum dylib от вызовов:

static std::unordered_map<const char *, Dylib> functionToDylib {{
   { "DoSomething", Dylib.NotOnStartup },
   { "FBSomeClass", Dylib.SomeOtherOne },
   ...
}};

Использование генерации кода было привлекательным по нескольким причинам:

Сочетание запроса Buck с генерацией кода оказалось настолько успешным, что FBiOS использовала его в качестве основы для новой системы плагинов, которая в конечном итоге заменила runtime-based систему модулей приложений.

Перемещение сигнала влево

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

Когда FBiOS собрана, Buck может создать график, показывающий расположение всех плагинов в приложении, например:

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

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

Стоимость генерации кода

Хотя миграция FBiOS на систему плагинов повысила надежность приложения, обеспечила более быструю работу для инженеров и позволила приложению легко обмениваться кодом с другими мобильными приложениями, за это пришлось заплатить:

Нет никаких сомнений в том, что плагины увели FBiOS дальше от идиоматической iOS-разработки, но компромиссы, похоже, того стоят. Наши инженеры могут изменить код, используемый во многих приложениях в Meta, и быть уверенными, что, если система плагинов работает, ни одно приложение не должно падать из-за отсутствия функциональности в редко тестируемом пути. Такие команды, как News Feed и Groups, могут создать точку расширения для плагинов и быть уверенными, что команды разработчиков продуктов смогут интегрироваться с ними, не касаясь основного кода.

2020: Swift и языковая архитектура

Хотя большая часть этой статьи посвящена архитектурным изменениям, связанным с проблемами масштабирования приложения Facebook, изменения в Apple SDK также вынудили FBiOS переосмыслить некоторые из своих архитектурных решений.

В 2020 году FBiOS начал замечать рост числа API-интерфейсов Apple, предназначенных только для Swift, и растущую потребность в большем количестве Swift в кодовой базе. Наконец-то пришло время смириться с тем фактом, что Swift стал неизбежным жителем в приложении FB.

Исторически сложилось так, что FBiOS использовала C++ в качестве рычага для создания абстракций, что позволяло экономить на размере кода из-за принципа нулевых накладных расходов C++. Но С++ не интероперабелен со Swift (пока). Для большинства API-интерфейсов FBiOS (таких как ComponentKit) для использования в Swift необходимо было бы создать какую-то прокладку, что привело бы к раздуванию кода.

Вот диаграмма, показывающая проблемы в кодовой базе:

Имея это в виду, мы начали формировать языковую стратегию, учитывающую то, когда и где следует использовать различные фрагменты кода:

В конце концов, команда FBiOS начала советовать, чтобы продуктовые API/код не содержали C++, чтобы мы могли свободно использовать Swift и будущие Swift API от Apple. Используя плагины, FBiOS могла абстрагироваться от реализаций C++, чтобы они по-прежнему подпитывали приложение, но были скрыты от большинства инженеров.

Этот тип рабочего процесса означал небольшой сдвиг в том, как инженеры FBiOS думали о построении абстракций. С 2014 года одними из самых важных факторов в построении фреймворка были вклады в размер и выразительность приложения (именно поэтому ComponentKit выбрал Objective-C++ вместо Objective-C).

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

2022: Путешествие завершено на 1%

С 2014 года архитектура FBiOS немного изменилась:

Тем временем Apple представила интересные улучшения для своих телефонов, ОС и SDK:

По мере того, как Facebook, Messenger, Instagram и WhatsApp обмениваются опытом, FBiOS пересматривает все эти оптимизации, чтобы увидеть, где приложение может приблизиться к ортодоксальности платформы. В конечном счете, мы увидели, что самый простой способ поделиться кодом — это использовать то, что приложение дает вам бесплатно, или создать что-то, что практически не зависит от зависимостей и может интегрироваться между всеми приложениями.

Увидимся здесь в 2032 году, чтобы подвести итоги 20-летия кодовой базы!

Источник

Exit mobile version