Событийно-ориентированная логика в Objective C держится на трех китах — протоколы, notification center и key-value observing. Традиционо протоколы используются для расширения функционала базовых классов без наследования, key-value observing – для взаимодействия между визуальной и логической частью приложения, а notification center — для обработкий событий пользователя.
Естественно, все это благообразие можно спокойно использовать для построения сложных приложений. Никакой реальной необходимости в изобретении собственных велосипедов, конечно же, нет. Однако мне, как человеку пришедшему в разработку Objective C приложений из мира .NET, показалось очень неприятным то, что notification center, который я планировал использовать для событий, разраывает стек приложения, записывая произошедшее событие в очередь в UI thread, а протоколы в классическом представлении не слишком удобны, посему для удобства я решил соорудить себе механизм, который был бы гораздо больше похож на то, чем мы привыкли обходиться в мире .NET. Так родился родилась идея реализации модели множественных подписантов через специальный класс, названный AWHandlersList.
Данная статья рассчитана на программистов, которые имеют определенный опыт в создании приложений на Objective C и уже писали подобные велосипеды, либо решали похожие задачи стандартными способами. Данный вариант не является silver bullet, но показал себя как удобный механизм, минимизирующий написание кода для обарботки множеств событий с разными интерфейсами и параметрами.
Идея класса довольно проста — он содержит в себе список подписантов, каждый элемент которого состоит из двух компонентов — target и selector.
Для чего создан этот велосипед? Мне показалось, что он удобнее, чем все представленные стандартные модели, для некоторой логики, связанной с передачей событий. Может быть, он кому-то поможет облегчить жизнь.
В .NET привычная модель событийной логики предлагает делегат с двумя параметрами — sender типа object и args типа, наследуемого от EventArgs. Чтобы не ломать себе
@interface AWEventArgs : NSObject
@end
Теперь определим класс, который будет содержать пару «целевой объект и вызываемый метод», добавив туда заодно некоторую отладочную информацию для того, чтобы в дальнейшем можно было легко отлаживать логику событий.
@interface AWEventHandler : NSObject
{
@private
NSString *_description;
}
@property (nonatomic, assign) id target;
@property (nonatomic, assign) SEL method;
+(AWEventHandler *)handlerWithTarget:(id)target method:(SEL)method;
@end
@implementation AWEventHandler
@synthesize method, target;
-(id)initWithTarget:(id)t method:(SEL)m;
{
self = [super init];
if(self)
{
target = t;
method = m;
_description = [[NSString alloc] initWithFormat:@"EventHandler, Target=%@, Method=%@", NSStringFromClass([target class]), NSStringFromSelector(method)];
}
return self;
}
-(NSString *)description
{
return _description;
}
-(void)dealloc
{
[_description release];
[super dealloc];
}
+(AWEventHandler *)handlerWithTarget:(id)target method:(SEL)method
{
AWEventHandler *handler = [[[AWEventHandler alloc] initWithTarget:target method:method] autorelease];
return handler;
}
@end
Как видите, и target, и method по сути представляют собой слабые ссылки. Это вполне закономерно — слабые ссылки повсеместно используются в мире Objective C для того, чтобы избежть circular references и дать возможность автоматически освобождать объекты. К сожалению, это приводит к тому, что при небрежном кодировании повсеместно появляются «мертвые» указатели на объекты, которые роняют приложение, поэтому я чуть дальше покажу один красивый механизм, который позволяет предупреждать и устранять их появление.
Теперь, наконец, перейдем к нашему основному классу — списку подписантов. В коде есть нетривиальные моменты, но они решаются чтением документации, а если желания разбираться в вопросе нет — то его можно просто использовать, код полностью рабочий и вынут из «боевого» проекта.
@interface AWEventHandlersList : NSObject
{
NSMutableArray *_handlers;
}
@property (nonatomic, copy) NSString *name;
-(void)addReceiver:(id)receiver delegate:(SEL)delegate;
-(void)removeReceiver:(id)receiver delegate:(SEL)delegate;
-(void)clearReceivers;
-(void)invoke;
-(void)invokeWithSender:(id)sender;
-(void)invokeWithSender:(id)sender args:(AWEventArgs *)event;
@property (nonatomic, retain) NSRunLoop *runLoop;
@end
Вкратце поясню, зачем нужны поля данного класса.
Первое — это name. Я предпочитаю именовать события, чтобы можно было увидеть в логах, какое именно событие было вызвано. Обычно в качестве имени события я использую имя класса вкупе с именем вызываемого в нем для выбрасывания (raise) метода. Это удобная практика, так как позволяет не рыскать судорожно по стеку в поисках того, кто событие выбросил, а просто в консоли отладки посмотреть это значение.
Методы addReceiver
и removeRecevier
логичны — они принимают объект и селектор, которые в дальнейшем будут принимать вызовы.
Методы invoke
должны выбрасывать событие, передавая его для обработки в подписанные объекты. Они даются в трех вариантах — для того, чтобы не передавать пустые значения nil в том случае, если в каикх-то параметрах события нет нужды.
Метод clearReceivers
внутренний, его лучше определять в анонимной секции, так как вызывающий код не должен иметь возможности отписывать другие объекты от событий, но исторически сложилось так, что он вынесен в интерфейс. Это легко исправить, если вам кажется это неправильным.
Наконец, свойство runLoop
необходимо в том случае, если вы собираетесь делать так, чтобы некоторые события были привязаны к определенному потоку (thread). Например, это необходимо, если существует какой-то код в worker thread должен обновлять визуальную часть приложения, либо наоборот — из UI thread должен быть доступ к какому-либо worker thread, синхронизируемому через очередь сообщений, то есть если есть необходимость выбрасывать события и обрабатывать их в разных потоках.
Реализация класса идейно тривиальна, однако требует некоторого понимания того, как работают селекторы. Сложные моменты я проясню в комментариях в самом коде.
@implementation AWEventHandlersList
@synthesize runLoop = _runLoop;
@synthesize name = _name;
-(id)init
{
self = [super init];
if(!self)
return nil;
_handlers = [[NSMutableArray alloc] init];
return self;
}
-(void)addReceiver:(id)receiver delegate:(SEL)delegate
{
/* Этот код можно убрать, если вы гарантируете, что каждый объект будет подписываться на событие только один раз, либо
* вам необходимо множественное подписание. Я предпочитаю работать со страховкой */
[self removeReceiver:receiver delegate:delegate];
[_handlers addObject:[AWEventHandler handlerWithTarget:receiver method:delegate]];
}
-(void)removeReceiver:(id)receiver delegate:(SEL)delegate
{
/* В идеале снятие копии со списка, сделанное для поддержки многопоточности, должно производитсья в критической секции
* (NSLock), однако я опустил этот момент, как как у меня подписание на события всегда происходит в одном потоке,
* а копия списка берется для того, чтобы в будущем достаточно было обернуть вызов в NSLock */
for(AWEventHandler *handler in [[_handlers copy] autorelease])
if(handler.method == delegate && handler.target == receiver)
[_handlers removeObject:handler];
}
-(void)clearReceivers
{
[_handlers removeAllObjects];
}
-(void)invoke
{
[self invokeWithSender:nil args:nil];
}
-(void)invokeWithSender:(id)sender
{
[self invokeWithSender:sender args:nil];
}
-(void)invokeWithSender:(id)sender args:(AWEventArgs *)event
{
[self invokeWithSender:sender args:event runLoop:_runLoop];
}
-(void)invokeWithSender:(id)sender args:(AWEventArgs *)event runLoop:(NSRunLoop *)runLoop
{
/* Вс случае, если к текущему потоку не привязан цикл выборки сообщений, метод вернет null и выполнеие будет
* происходить по обычному сценарию */
if(!runLoop)
runLoop = [NSRunLoop currentRunLoop];
NSUInteger order = 1;
NSArray *handlersCopy = [NSArray arrayWithArray:_handlers];
for(AWEventHandler *handler in handlersCopy)
if(runLoop == [NSRunLoop currentRunLoop])
[self internalInvoke:[NSArray arrayWithObjects:handler, sender == nil ? [NSNull null] : sender, event == nil ? [NSNull null] : event, nil]];
else
[runLoop performSelector:@selector(internalInvoke:) target:self argument:[NSArray arrayWithObjects:handler, sender == nil ? [NSNull null] : sender, event == nil ? [NSNull null] : event, nil] order:order++ modes:[NSArray arrayWithObject:NSDefaultRunLoopMode]];
}
/* Передача объектов производится через массив для возможности работы с потоками через performSelector:target:argument:order:modes: */
-(void)internalInvoke:(NSArray *)data
{
AWEventHandler *handler = [data objectAtIndex:0];
id sender = [data objectAtIndex:1];
if(sender == [NSNull null])
sender = nil;
id args = [data objectAtIndex:2];
if(args == [NSNull null])
args = nil;
/* Данный класс используется для анализа сигнатуры метода и определения потребного числа параметров его вызова */
NSMethodSignature *mSig = [handler.target methodSignatureForSelector:handler.method];
if([mSig numberOfArguments] == 2)
[handler.target performSelector:handler.method];
else if([mSig numberOfArguments] == 3)
[handler.target performSelector:handler.method withObject:sender];
else if ([mSig numberOfArguments] == 4)
[handler.target performSelector:handler.method withObject:sender withObject:args];
else
@throw [NSException exceptionWithName:@"Invalid selector type" reason:@"This type of selector is not supported" userInfo:nil];
}
-(void)dealloc
{
self.name = nil;
[self clearReceivers];
[_handlers release];
[super dealloc];
}
@end
Теперь определим пару вспомогательных макросов, которые дадут нам возможность встраивать логику работы с событиями в класс буквально двумя строчками.
#define DEFINE_EVENT(eventName)
-(void)add ## eventName ## Handler:(id)receiver action:(SEL)action;
-(void)remove ## eventName ## Handler:(id)receiver action:(SEL)action
#define DEFINE_EVENT_IMPL(eventName, innerVariable)
-(void)add ## eventName ## Handler:(id)receiver action:(SEL)action
{
[innerVariable addReceiver:receiver delegate:action];
}
-(void)remove ## eventName ## Handler:(id)receiver action:(SEL)action
{
[innerVariable removeReceiver:receiver delegate:action] ;
}
Теперь для того, чтобы создать в классе событие, нужно определить внутреннюю списочную переменную:
AWEventHandlersList *_handlers;
Определить событие в интерфейсе
DEFINE_EVENT(Event);
И связать список с событием
DEFINE_EVENT_IMPL(Event, _handlers)
В классе автоматически добавляются два метода — addEventHandler:action:
и removeEventHandler:action:
, а вызвать событие можно через методы invoke
объекта _handlers
.
Конечно, не стоит забывать о том, что объект _handlers нужно инициализировать в конструкторе
_handlers = [AWEventHandlersList new];
И уничтожать в деструкторе объекта
[_handlers release];
Во второй части статьи я расскажу, к каким проблемам ведет использование этого подхода и как справляться с трудностями «мертвых» ссылок, которые возникают в любом мало-мальски объемном приложении в результате наших собственных ошибок.
Автор: bobermaniac