Поиск таких объектов мог быть затруднен, если бы не замечательная в отладке вещь под названием 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/
Добавить комментарий