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

в 8:09, , рубрики: event handling, Events, обработка событий, события, метки: , , , ,

Событийно-ориентированная логика в 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];

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

Автор: bobermaniac

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js