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