В прошлой части цикла мы познакомились с Dependency Injection фреймворком для iOS — Typhoon, и рассмотрели базовые примеры его использования в проекте Рамблер.Почта. В этот раз мы углубимся в изучение его внутреннего устройства.
Цикл «Управляем зависимостями в iOS-приложениях правильно»
- Знакомство с Typhoon
- Устройство Typhoon
- Модульность Typhoon
- Typhoon Tips n' Tricks
- Альтернативы Typhoon
- (Дополнительно) Rambler.iOS #3. Dependency Injection в iOS. Слайды
- (Дополнительно) Rambler.iOS #3. Dependency Injection в iOS. Видео
Введение
Для начала разберем небольшой словарь терминов, которые будут активно использоваться в этой статье:
- Assembly (читается как [эссэмбли]). Ближайший русский эквивалент — сборка, конструкция. В Typhoon — это объекты, содержащие в себе конфигурации всех зависимостей приложения, по сути своей являются костяком всей архитектуры. Для внешнего мира, будучи активированными, ведут себя как обычные фабрики.
- Definition. Что касается перевода на русский язык — мне больше всего импонирует конфигурация, как наиболее близкий к оригиналу вариант. TyphoonDefinition — это объекты, являющиеся своеобразной моделью зависимостей, содержат в себе такую информацию, как класс создаваемого объекта, его свойства, тип жизненного цикла. Большинство примеров из предыдущей статьи касались как раз таки различных вариантов настройки TyphoonDefinition.
- Scope. Здесь все просто — это тип жизненного цикла объекта, созданного при помощи Typhoon.
- Активация. Процесс, в результате которого все объекты-наследники TyphoonAssemby начинают вместо TyphoonDefinition отдавать реальные инстансы классов. Суть и принцип работы активации рассмотрим чуть ниже.
И еще раз делаю упор на том, что очень важно разобраться в базовых принципах работы фреймворка — после этого мы спокойно сможем двинуться дальше и изучить все прочие плюшки Typhoon, не останавливаясь на деталях их реализации.
Чтобы не захламлять статью огромными листингами кода, я буду периодически ссылаться на определенные файлы фреймворка, а приводить лишь самые интересные моменты. Обращаю ваше внимание на то, что актуальная версия Typhoon Framework на момент написания статьи — 3.1.7.
Инициализация
Жизненный цикл приложения с использованием Typhoon выглядит следующим образом:
- Вызов main.m
- Создание UIApplication — [UIApplication init]
- Создание UIAppDelegate — [UIAppDelegate init]
- Вызов метода setDelegate: у созданного инстанса UIApplication
- Вызов засвиззленной в классе TyphoonStartup имплементации setDelegate:
- Вызов метода -applicationDidFinishLaunching:withOptions: у инстанса UIAppDelegate
Именно в засвиззленном setDelegate: и происходит создание и активация стартовых assemblies.
Автоматическая загрузка фабрик возможна в двух случаях: мы либо указали их классы в Info.plist под ключом TyphoonInitialAssemblies:
+ (id)factoryFromPlistInBundle:(NSBundle *)bundle
+ (id)factoryFromPlistInBundle:(NSBundle *)bundle
{
TyphoonComponentFactory *result = nil;
NSArray *assemblyNames = [self plistAssemblyNames:bundle];
NSAssert(!assemblyNames || [assemblyNames isKindOfClass:[NSArray class]],
@"Value for 'TyphoonInitialAssemblies' key must be array");
if ([assemblyNames count] > 0) {
NSMutableArray *assemblies = [[NSMutableArray alloc] initWithCapacity:[assemblyNames count]];
for (NSString *assemblyName in assemblyNames) {
Class cls = TyphoonClassFromString(assemblyName);
if (!cls) {
[NSException raise:NSInvalidArgumentException format:@"Can't resolve assembly for name %@",
assemblyName];
}
[assemblies addObject:[cls assembly]];
}
result = [TyphoonBlockComponentFactory factoryWithAssemblies:assemblies];
}
return result;
}
либо реализовали метод -initialFactory в нашем AppDelegate:
+ (TyphoonComponentFactory *)factoryFromAppDelegate:(id)appDelegate
+ (TyphoonComponentFactory *)factoryFromAppDelegate:(id)appDelegate
{
TyphoonComponentFactory *result = nil;
if ([appDelegate respondsToSelector:@selector(initialFactory)]) {
result = [appDelegate initialFactory];
}
return result;
}
Если не было сделано ни того, ни другого — assembly придется создавать руками в каком-либо другом месте кода, что делать не рекомендуется.
Больше о деталях инициализации Typhoon можно узнать в следующих исходных файлах:
- TyphoonStartup.m
- TyphoonComponentFactory.m
Активация
Этот процесс является ключевым в работе фреймворка. Под активацией понимается создание объекта класса TyphoonBlockComponentFactory, инстанс которого находится «под капотом» у всех активированных assembly. Таким образом, любая assembly играет роль интерфейса для общения с настоящей фабрикой.
Посмотрим, что происходит, не особо вдаваясь в подробности:
- У TyphoonBlockComponentFactory вызывается инициализатор -initWithAssemblies:, на вход которому передается массив assembly, которые нужно активировать.
- Каждому из definition'ов, создаваемых активируемыми assembly, назначается свой уникальный ключ (рандомная строка + имя метода).
- Все TyphoonDefinition добавляются в массив registry только что созданного TyphoonBlockComponentFactory.
Конечно, этими тремя пунктами дело не ограничивается: для каждого зарегистрированного TyphoonDefinition добавляются аспекты, геттеры всех зависимостей свиззлятся, создавая тем самым цепочку инициализации графа объектов, в TyphoonBlockComponentFactory создаются пулы инстансов — в общем и целом, для обеспечения работы фреймворка производится большое количество различных действий. В рамках этой статьи мы не будем вдаваться в подробности каждой из рассматриваемых процедур, так как это может отвлечь от понимания общих принципов работы Typhoon.
Мы рассмотрели, как TyphoonAssembly активируется — осталось понять, зачем это вообще нужно делать. Каждый раз, когда мы вручную дергаем у assembly какой-нибудь метод, отдающий TyphoonDefinition для зависимости, происходит следующее:
- (void)forwardInvocation:(NSInvocation *)anInvocation
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (_factory) {
[_factory forwardInvocation:anInvocation];
}
...
}
В _factory полученный NSInvocation обрабатывается и преобразуется в вызов следующего метода:
- (id)componentForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args
- (id)componentForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args
{
if (!key) {
return nil;
}
[self loadIfNeeded];
TyphoonDefinition *definition = [self definitionForKey:key];
if (!definition) {
[NSException raise:NSInvalidArgumentException format:@"No component matching id '%@'.", key];
}
return [self newOrScopeCachedInstanceForDefinition:definition args:args];
}
По сгенерированному из selector'а метода ключу достается один из зарегистрированных в TyphoonBlockComponentFactory definition'ов, и затем на его основе либо создается новый инстанс, либо переиспользуется закэшированный.
Больше о процедуре активации можно узнать в следующих исходных файлах:
- TyphoonAssembly.m
- TyphoonBlockComponentFactory.m
- TyphoonTypeDescriptor.m
- TyphoonAssemblyDefinitionBuilder.m
- TyphoonStackElement.m
Работа со Storyboard
Под капотом Typhoon использует свой сабкласс UIStoryboard — TyphoonStoryboard. Первая особенность, бросающаяся в глаза — это фабричный метод, который отличается от своего родителя дополнительным параметром — factory:
+ (TyphoonStoryboard *)storyboardWithName:(NSString *)name
factory:(id<TyphoonComponentFactory>)factory
bundle:(NSBundle *)bundleOrNil;
Именно в этой фабрике, реализующей протокол TyphoonComponentFactory, будет осуществляться поиск definition'ов для экранов текущей storyboard. Посмотрим на все этапы инжекции зависимостей во ViewController'ы:
- В первую очередь мы попадаем в метод -instantiateViewControllerWithIdentifier:, переопределенный в TyphoonStoryboard.
- Создается инстанс нужного контроллера путем вызова super'a.
- Инициируется инжекция всех зависимостей текущего контроллера и его дочерних контроллеров:
- (void)injectPropertiesForViewController:(UIViewController *)viewController- (void)injectPropertiesForViewController:(UIViewController *)viewController { if (viewController.typhoonKey.length > 0) { [self.factory inject:viewController withSelector:NSSelectorFromString(viewController.typhoonKey)]; } else { [self.factory inject:viewController]; } for (UIViewController *controller in viewController.childViewControllers) { [self injectPropertiesForViewController:controller]; } }
- В TyphoonBlockComponentFactory происходит уже знакомая нам процедура — ищется соответствующий текущему классу TyphoonDefinition и инициируется процесс инжекции в нее графа зависимостей.
Сейчас я не буду останавливаться на конкретной реализации работы с TyphoonStoryboard в приложении — эта тема будет затронута в одной из следующих статей.
Подробнее о реализации работы со storyboard можно узнать в следующих исходных файлах:
- TyphoonStoryboard.m
- TyphoonBlockComponentFactory.m
TyphoonDefinition
Практически в каждом приведенном мною сниппете в том или ином виде встречается класс TyphoonDefinition. Как я уже упоминал при перечислении терминов, TyphoonDefinition — это своего рода конфигурационный класс для создаваемой зависимости — поэтому для нас в первую очередь представляет интерес именно его интерфейс:
- Class _type — класс создаваемой зависимости
- NSString *_key — уникальный ключ, генерируемый при активации Typhoon,
- TyphoonMethod *_initializer — объект, создаваемый при initializer injection, содержащий в себе сигнатуру нужного инициализатора и коллекцию его параметров,
- TyphoonMethod *_beforeInjections — метод, который будет вызван до проведения инъекции зависимостей,
- TyphoonMethod *_afterInjections — метод, который будет вызван после проведения инъекции зависимостей,
- NSMutableSet *_injectedProperties — коллекция зависимостей, устанавливаемых через property injection,
- NSMutableOrderedSet *_injectedMethods — коллекция методов, в которые передаются определенные зависимости (method injection),
- TyphoonScope scope — тип жизненного цикла создаваемого объекта,
- TyphoonDefinition *_parent — базовый TyphoonDefinition, все свойства которой будут унаследованы текущей,
- BOOL abstract — флаг, указывающий на то, что текущая конфигурация может использоваться только для реализации наследования, и представляемый ею объект никогда не должен создаваться напрямую.
Из всех вышеперечисленных свойств отдельного внимания заслуживает scope объекта.
Подробнее о принципах работы TyphoonDefinition можно узнать в следующих исходных файлах:
- TyphoonDefinition.m
- TyphoonAssemblyDefinitionBuilder.m
- TyphoonFactoryDefinition.m
- TyphoonInjectionByReference.m
- TyphoonMethod.m
TyphoonScope
Важно четко понимать, что, говоря о разных типах жизненного цикла объекта, мы все равно жестко привязаны к lifetime используемого инстанса TyphoonBlockComponentFactory — если эта фабрика будет высвобождена из памяти, вместе с ней освободятся и все графы объектов.
Посмотрим, к чему приводит каждое из значений TyphoonScope:
- TyphoonScopeObjectGraph
В процессе построения графа зависимостей объект с таким scope будет создан только один раз. Технически это выглядит так: перед созданием инстанса (какого-либо definition), создается пул для object graph scoped объектов, в процессе построения графа зависимостей, все object-graph-scoped уходят в этот пул, а после того, как инстанс создан, этот пул очищается. - TyphoonScopePrototype
При каждом обращении к TyphoonDefinition с таким scope будет создаваться новый инстанс класса. - TyphoonScopeSingleton
Объект с таким жизненным циклом будет жить на всем протяжении жизни TyphoonComponentFactory. - TyphoonScopeLazySingleton
Как видно из названия — это синглтон, который будет создан в момент первого обращения к нему. - TyphoonScopeWeakSingleton
Синглтон, создаваемый при использовании такого TyphoonDefinition, находится в памяти ровно до тех пор, пока на него ссылается хотя бы один объект — в противном случае, он будет освобожден.
Созданный объект, в зависимости от свойства scope его конфигурации, хранится в одном из пулов TyphoonComponentFactory, каждый из которых работает определенным образом.
Больше о принципах работы кэша зависимостей Typhoon можно узнать в следующих исходниках:
- TyphoonComponentFactory.m
- TyphoonWeakComponentPool.m
- TyphoonCallStack.m
Заключение
Мы успели рассмотреть только самые базовые принципы работы Typhoon Framework — инициализацию, активацию фабрик, устройство TyphoonAssembly, TyphoonStoryboard, TyphoonDefinition и TyphoonBlockComponentFactory, особенности жизненного цикла создаваемых объектов. Библиотека содержит в себе еще очень много интересных концепций, реализация которых порой просто завораживает.
Я настоятельно рекомендую уделить несколько дней и зарыться в их изучение с головой — это более чем достойная альтернатива изучению многочисленных уроков в стиле «Работаем в Xcode мышкой на Swift бесплатно и без СМС» и «Продвинутая анимация индикатора загрузки файлов».
В следующей части цикла вы узнаете, как избежать появления одной огромной фабрики, правильно разбить архитектуру уровня Assembly на модули и покрыть все это дело тестами.
Цикл «Управляем зависимостями в iOS-приложениях правильно»
- Знакомство с Typhoon
- Устройство Typhoon
- Модульность Typhoon
- Typhoon Tips n' Tricks
- Альтернативы Typhoon
- (Дополнительно) Rambler.iOS #3. Dependency Injection в iOS. Слайды
- (Дополнительно) Rambler.iOS #3. Dependency Injection в iOS. Видео