Событийно-ориентированная логика в 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. Чтобы не ломать себе мозг, сделаем то же самое. Для начала, определим пустой класс EventArgs, от которого будут наследоваться все аргументы событий.
Теперь определим класс, который будет содержать пару «целевой объект и вызываемый метод», добавив туда заодно некоторую отладочную информацию для того, чтобы в дальнейшем можно было легко отлаживать логику событий.
Как видите, и target, и method по сути представляют собой слабые ссылки. Это вполне закономерно — слабые ссылки повсеместно используются в мире Objective C для того, чтобы избежть circular references и дать возможность автоматически освобождать объекты. К сожалению, это приводит к тому, что при небрежном кодировании повсеместно появляются «мертвые» указатели на объекты, которые роняют приложение, поэтому я чуть дальше покажу один красивый механизм, который позволяет предупреждать и устранять их появление.
Теперь, наконец, перейдем к нашему основному классу — списку подписантов. В коде есть нетривиальные моменты, но они решаются чтением документации, а если желания разбираться в вопросе нет — то его можно просто использовать, код полностью рабочий и вынут из «боевого» проекта.
Вкратце поясню, зачем нужны поля данного класса.
Первое — это name. Я предпочитаю именовать события, чтобы можно было увидеть в логах, какое именно событие было вызвано. Обычно в качестве имени события я использую имя класса вкупе с именем вызываемого в нем для выбрасывания (raise) метода. Это удобная практика, так как позволяет не рыскать судорожно по стеку в поисках того, кто событие выбросил, а просто в консоли отладки посмотреть это значение.
Методы
Методы
Метод
Наконец, свойство
Реализация класса идейно тривиальна, однако требует некоторого понимания того, как работают селекторы. Сложные моменты я проясню в комментариях в самом коде.
Теперь определим пару вспомогательных макросов, которые дадут нам возможность встраивать логику работы с событиями в класс буквально двумя строчками.
Теперь для того, чтобы создать в классе событие, нужно определить внутреннюю списочную переменную:
Определить событие в интерфейсе
И связать список с событием
В классе автоматически добавляются два метода —
Конечно, не стоит забывать о том, что объект _handlers нужно инициализировать в конструкторе
И уничтожать в деструкторе объекта
Во второй части статьи я расскажу, к каким проблемам ведет использование этого подхода и как справляться с трудностями «мертвых» ссылок, которые возникают в любом мало-мальски объемном приложении в результате наших собственных ошибок.
Естественно, все это благообразие можно спокойно использовать для построения сложных приложений. Никакой реальной необходимости в изобретении собственных велосипедов, конечно же, нет. Однако мне, как человеку пришедшему в разработку Objective C приложений из мира .NET, показалось очень неприятным то, что notification center, который я планировал использовать для событий, разраывает стек приложения, записывая произошедшее событие в очередь в UI thread, а протоколы в классическом представлении не слишком удобны, посему для удобства я решил соорудить себе механизм, который был бы гораздо больше похож на то, чем мы привыкли обходиться в мире .NET. Так родился родилась идея реализации модели множественных подписантов через специальный класс, названный AWHandlersList.
Данная статья рассчитана на программистов, которые имеют определенный опыт в создании приложений на Objective C и уже писали подобные велосипеды, либо решали похожие задачи стандартными способами. Данный вариант не является silver bullet, но показал себя как удобный механизм, минимизирующий написание кода для обарботки множеств событий с разными интерфейсами и параметрами.
Идея класса довольно проста — он содержит в себе список подписантов, каждый элемент которого состоит из двух компонентов — target и selector.
Для чего создан этот велосипед? Мне показалось, что он удобнее, чем все представленные стандартные модели, для некоторой логики, связанной с передачей событий. Может быть, он кому-то поможет облегчить жизнь.
В .NET привычная модель событийной логики предлагает делегат с двумя параметрами — sender типа object и args типа, наследуемого от EventArgs. Чтобы не ломать себе мозг, сделаем то же самое. Для начала, определим пустой класс 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];
Во второй части статьи я расскажу, к каким проблемам ведет использование этого подхода и как справляться с трудностями «мертвых» ссылок, которые возникают в любом мало-мальски объемном приложении в результате наших собственных ошибок.