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

    Комментарии 1

      0
      Или же тихо отписать observer, аналогичное решение в: iAsync

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

      Самое читаемое