Objective C. Практика. События

от автора

Событийно-ориентированная логика в 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, от которого будут наследоваться все аргументы событий.

@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];

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

ссылка на оригинал статьи http://habrahabr.ru/post/168179/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *