Objective C. Практика. События и «мертвые» объекты

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

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

Поиск таких объектов мог быть затруднен, если бы не замечательная в отладке вещь под названием NSKVODeallocateBreak, которая позволяет прервать выполнение приложения в момент, когда уничтожается объект, подписанный на события, чтобы отследить время его жизни и убрать проблему.

В процессе работы над классом, который я использовал для событий, мне захотелось создать аналогичный механизм, так как ошибки в событийной логике довольно сложно прогнозировать, и страховка здесь не помешает.

Данная статья рассчитана на разработчиков, имеющих опыт работы с платформой и знающих, каким образом определяется жизненный цикл объекта. Если у вас есть определенные пробелы в этой области (а я неоднократно встречал даже опытных разработчиков, которые не знают, каким образом работает счетчик ссылок и не представляющих, во что разворачивается @synthesize), то вы можете прочитать мою старую статью, посвященную исследованию данного вопроса. Остальных прошу к столу.

Итак, чего мы хотим? Мы хотим, чтобы как только объект, который подписан на событие через механизм, описанный в предыдущем выпуске, уничтожался, мы получали информацию об этом в отладчике.

Что для этого нужно? Очевидное решение — каким-то образом при подписке на событие перехватывать вызов dealloc объекта-подписанта и сообщать об этом разработчику. Однако, вот незадача — невозможно штатными средствами перехватить dealloc (или, по крайней мере, я такого способа не нашел).

К счастью, Objective C дает возможность довольно красиво обойти это ограничение с помощью своего runtime. Идею этого решения я подсмотрел в заметке некого codeshaker, и она оказалась невероятно красива и элегантна. Переделав ее под свои нужды, я получил следующий код:

@interface NSObject (NSObjectDeallocInfo)

-(void)dealloc_override;

@end

@implementation NSObject (NSObjectDeallocInfo)

+(void)load
{
	method_exchangeImplementations(class_getInstanceMethod(self, @selector(dealloc)), class_getInstanceMethod(self, @selector(dealloc_override)));
}

-(void)dealloc_override
{
	[self dealloc_override];
}

@end

Фактически этот код заменяет метод-обработчик сообщения dealloc на наш обработчик dealloc_override, а кажущийся рекурсивным вызов [self dealloc_override] на самом деле теперь ведет в стандартный метод.

Второй вопрос — это где хранить информацию о связи нашего объекта-подписанта с объектом-событием. Использовать статический словарь? Нет, это значит увеличить число проблем. К счастью, runtime нам и здесь поможет — оказывается, что с любым объектом в Objective C уже связан словарь для свойств-расширений и нам нужно просто его задействовать.

Определим некоторый уникальный идентификатор нашего свойства.

static void* AW_EVENTHANDLER_KEY = (void *)0x2781;

Тепреь привяжем его к классу NSObject, используя ту же самую категорию, которой мы воспользовались для переопределения метода.

@interface NSObject (NSObjectDeallocInfo)

@property (nonatomic, assign) AWEventHandlersList *attachedEventHandler;
-(void)dealloc_override;

@end

@implementation NSObject (NSObjectDeallocInfo)

+(void)load
{
	method_exchangeImplementations(class_getInstanceMethod(self, @selector(dealloc)), class_getInstanceMethod(self, @selector(dealloc_override)));
}

-(void)dealloc_override
{
	[self dealloc_override];
}

-(AWEventHandlersList *)attachedEventHandler
{
	return (AWEventHandlersList *)objc_getAssociatedObject(self, AW_EVENTHANDLER_KEY);
}

-(void)setAttachedEventHandler:(AWEventHandlersList *)attachedEventHandler
{
	objc_setAssociatedObject(self, AW_EVENTHANDLER_KEY, attachedEventHandler, OBJC_ASSOCIATION_ASSIGN);
}

@end

Теперь у любого объекта типа NSObject есть виртуальное свойство attachedEventHandler, которое мы будем использовать для хранения нужной нам информации.

Расширим код класса AWEventHandlersList, чтобы он записывал в это поле объект привязанного события и добавим метод, возвращающий YES если объект подписан на событие.

-(void)addReceiver:(id)receiver delegate:(SEL)delegate
{
	[self removeReceiver:receiver delegate:delegate];
	
	[receiver setAttachedEventHandler:self];
	[_handlers addObject:[AWEventHandler handlerWithTarget:receiver method:delegate]];
}

-(void)removeReceiver:(id)receiver delegate:(SEL)delegate
{
	[receiver setAttachedEventHandler:nil];
	for(AWEventHandler *handler in [[_handlers copy] autorelease])
		if(handler.method == delegate && handler.target == receiver)
			[_handlers removeObject:handler];
}

-(BOOL)isReceiverInList:(id)receiver
{
	for(AWEventHandler *handler in _handlers)
		if(handler.target == receiver)
			return YES;
	return NO;
}

-(void)clearReceivers
{
	for(AWEventHandler *handler in _handlers)
		[handler.target setAttachedEventHandler:nil];
	[_handlers removeAllObjects];
}

Теперь становится довольно просто реализовать то, ради чего это затевалось — проверку на момент деаллокации объекта.

-(void)dealloc_override
{
	AWEventHandlersList *handler = self.attachedEventHandler;
	if(handler)
		if([handler isReceiverInList:self])
		{
			NSLog(@"Event handler (%@) target is released while subscribed", handler.name);
			[NSException raise:@"E_HANDLERRELEASED" format:@"Event handler (%@) target is released while subscribed", handler.name];
		}
	[self dealloc_override];
}

Теперь в случае, если объект был уничтожен, пока он подписан на событие, то будет выбрасываться исключение. Что самое приятное — исключение выбрасывается в месте, где срабатывает деструктор объекта, что позволяет видеть стек ошибки в crash reports.

Таким образом, мы всего за 15 минут сделали нашу отладочную жизнь несколько проще.

Автор: bobermaniac

Источник

Поделиться

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