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

от автора

Многие, вероятно, знают, что при работе с событиями изменения свойств с помощью 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 минут сделали нашу отладочную жизнь несколько проще.

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


Комментарии

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

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