POSScheduling
Библиотека POSScheduling являет собой пример реализации одноименного паттерна. В таблице ниже приведено соответствие компонентов паттерна и реализующих их механизмов.
| Компонент | Реализация |
|---|---|
| События | Блоки Objective-C |
| Очередь событий | Внутренняя реализация dispatch_queue_t из Grand Central Dispatch |
| Цикл обработки событий | Внутренняя реализация dispatch_queue_t из Grand Central Dispatch |
| Поток | Внутренняя реализация dispatch_queue_t из Grand Central Dispatch |
| Планировщик | RACTargetQueueScheduler из ReactiveCocoa |
Центральная часть библиотеки – класс POSSchedulableObject. Будучи базовым классом для управляемых
объектов, он берет на себя следующие обязанности:
- Имеет ссылку на планировщик, через который должно происходить косвенное взаимодействие с объектом.
- Автоматически проверяет корректность потока, из которого происходит вызов методов объекта-наследника. Достигается это за счет навешивания хуков (hooks) на все его методы в момент инициализации. В виду дороговизны данной процедуры по умолчанию она осуществляется только в отладочной версии приложения.
Основную часть исходников репозитория составляет демо-приложение. Оно авторизует пользователя в сервисе Dropbox, после чего выводит на экран имя и фамилию из его профиля. Рассмотрим несколько образцово-показательных примеров использования им библиотеки POSSchedulableObject.
###Объявление класса
/// Providers info about account.
@protocol SODAccountInfoProvider <POSSchedulableObject>
/// @return Signal of nonnull SODAccountInfo.
- (RACSignal *)fetchAccountInfo;
@end
@interface SODDropboxAccountInfoProvider
: POSSchedulableObject <SODAccountInfoProvider>
// ...
@endПротокол POSSchedulable содержит методы для отправки событий управляемому объекту, обрабатывающихся
в другом потоке.
@protocol POSSchedulable <NSObject>
/// Scheduler which is used to perform calls to objects of that class.
@property (nonatomic, readonly) RACTargetQueueScheduler *scheduler;
/// @return Signal with this nonnull object delivered in the object's scheduler.
- (RACSignal *)schedule;
/// Schedules that object in the object's scheduler.
- (void)scheduleBlock:(void (^)(id schedulable))block;
@endКласс POSSchedulableObject полностью реализует одноименный протокол. Кроме того, он добавляет проверки
на предмет того, что методы объекта вызываются в правильном потоке. Из проверок исключаются свойства с
атрибутом atomic. Для ручного исключения тех или иных методов существует специальный инициализатор с
настройками исключений.
@interface POSSchedulableObject : NSObject <POSSchedulableObject>
/// Schedules object inside main thread scheduler.
- (instancetype)init;
/// Schedules object inside specified scheduler.
- (instancetype)initWithScheduler:(RACTargetQueueScheduler *)scheduler;
/// Schedules object inside specified scheduler with custom excludes.
- (instancetype)initWithScheduler:(RACTargetQueueScheduler *)scheduler
options:(nullable POSScheduleProtectionOptions *)options;
// ...
@end###Взаимодействие с классом
Листинг ниже показывает, как получить результат косвенного вызова и воспользоваться им в контексте потока вызвавшего его объекта.
@implementation SODSettingsViewController
// ...
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[[[[[_assembly.accountInfoProvider schedule]
flattenMap:^RACStream *(id<SODAccountInfoProvider> provider) {
return [provider fetchAccountInfo];
}]
deliverOnMainThread]
takeUntil:self.rac_willDeallocSignal]
subscribeNext:^(SODAccountInfo *accountInfo) {
self.nameLabel.text = accountInfo.displayName;
} error:^(NSError *error) {
self.nameLabel.text = error.localizedDescription;
}];
}
// ...
@endОбращение к сервису provider путем отправки ему сообщения с использованием его планировщика. Далее результат
или ошибка перенаправляется обратно в главный цикл приложения.
###Сборка классов
Все объекты бизнес-логики приложения создаются внутри специальных классов, реализующих паттерн Dependency Injection Container. По аналогии с популярной библиотекой Typhoon, в названиях таких классов фигурирует корень Assembly. Создание объектов внутри них имеет две особенности:
- Объекты создаются лениво, по запросу. Следствием этого является изменяющееся на протяжении времени жизни
состояние объектов Assembly. Оно также защищается от многопоточного доступа путем наследования от
POSSchedulableObject. - Возврат объектов попросившей их стороне происходит синхронно во избежании большого количества клиентского
boilerplate-кода. Как видно из предыдущего листинга, использование
accountInfoProviderдостаточно многословно. Несложно представить, как приведенный код мог бы еще больше усложниться, если бы интерфейс Assembly имел асинхронную природу.
Таким образом, Assembly обязуется создать и вернуть любой сервис синхронно и только в главном потоке. Создание объектов выглядит примерно следующим образом:
@protocol SODAccountAssembly <POSSchedulableObject>
// ...
@property (nonatomic, readonly) id<SODAccountInfoProvider> accountInfoProvider;
@property (nonatomic, readonly) id<SODNodeRepository> nodeRepository;
// ...
@end
@interface SODDropboxAccountAssembly : POSSchedulableObject <SODAccountAssembly>
// ...
@end
@implementation SODDropboxAccountAssembly
// ...
- (id<PFYAccountInfoProvider>)accountInfoProvider {
if (_accountInfoProvider) {
return _accountInfoProvider;
}
self.accountInfoProvider = [[PFYDropboxAccountInfoProvider alloc]
initWithHost:self.dropboxHost
accountID:self.account.ID];
return _accountInfoProvider;
}
// ...
@endВсе выглядит достаточно просто пока вдруг не потребуется создать граф объектов, которые, во-первых, живут в разных потоках, а во-вторых, для своей инициализации требуют вызвать один или несколько своих методов.
Проблема в этом сценарии состоит в том, что для инициализации объекта A необходимо в красном потоке инициализировать объект B. Для того, чтобы с точки зрения клиента Assembly это произошло синхронно, на время создания красного объекта B синий поток должен быть заблокирован. Однако объекту B нужен объект C. Последний может создаться только в синем потоке. По аналогии с предыдущим шагом, на время его создания красный поток блокируется и ожидает завершения создания объекта C. Ожидание на этом этапе будет длиться вечно, поскольку событие, отправленное в синий поток, никогда не будет обработано, поскольку он был заблокирован при создании объекта A.
Выход из сложившейся ситуации заключается в том, чтобы блокировать поток с помощью специальной spin-блокировки.
Она должна останавливать исполнение текущего потока, но при этом осуществлять прокручивание его цикла обработки
сообщений. В рамках библиотеки POSSchedulableObject специально для этого случая предусмотрен метод posrx_await
в категории к RACSignal.
@implementation RACSignal (POSSchedulableObject)
- (id)posrx_await {
__block id result = nil;
__block BOOL done = NO;
[[self take:1] subscribeNext:^(id value) {
result = value;
done = YES;
} error:^(NSError *e) {
done = YES;
}];
if (result) {
return result;
}
NSRunLoop *runLoop = NSRunLoop.currentRunLoop;
while ([runLoop runMode:NSDefaultRunLoopMode beforeDate:NSDate.date] && !done) {}
return result;
}
@endВ демо-приложении он используется для создания объекта tracker:
- (id<PFYTracker>)tracker {
if (_tracker) {
return _tracker;
}
PFYAppTracker *tracker = [[PFYAppTracker alloc]
initWithScheduler:self.backgroundScheduler
store:self.secureStore
environment:self.environment];
self.tracker = [[[tracker schedule] map:^id(PFYAppTracker *scheduledTracker) {
[scheduledTracker addService:
[[PFYConsoleTracker alloc] initWithScheduler:scheduledTracker.scheduler]];
return scheduledTracker;
}] posrx_await];
return _tracker;
}##Ссылки
