Когда связанный объект освобождается?

Я присоединяю объект B через ассоциативную ссылку к объекту A. Объект B наблюдает некоторые свойства объекта A через KVO.

Проблема в том, что объект B кажется освобожденным после объекта A, что означает, что уже слишком поздно удалять себя в качестве наблюдателя KVO объекта A. Я знаю это, потому что получаю исключения NSKVODeallocateBreak, за которыми следуют аварийные сбои EXEC_BAD_ACCESS в dealloc объекта B.

Кто-нибудь знает, почему объект B освобождается после объекта A с OBJC_ASSOCIATION_RETAIN? Связанные объекты освобождаются после освобождения? Получают ли они авто-релиз? Кто-нибудь знает способ изменить это поведение?

Я пытаюсь добавить некоторые вещи в класс через категории, поэтому я не могу переопределить любые существующие методы (включая dealloc), и я не особенно хочу возиться с перебором. Мне нужен какой-то способ отсоединения и освобождения объекта B, прежде чем объект A будет освобожден.

РЕДАКТИРОВАТЬ - Вот код, который я пытаюсь заставить работать. Если связанные объекты были освобождены до полного освобождения UIImageView, все это будет работать. Единственное решение, которое я вижу, - это использовать свой собственный метод dealloc и вернуть обратно оригинал, чтобы вызвать его. Это становится действительно грязным, хотя.

Смысл класса ZSPropertyWatcher в том, что KVO требуется стандартный метод обратного вызова, и я не хочу заменять UIImageView, если он сам его использует.

UIImageView + Loading.h

@interface UIImageView (ZSShowLoading)
@property (nonatomic)   BOOL    showLoadingSpinner;
@end

UIImageView + Loading.m

@implementation UIImageView (ZSShowLoading)

#define UIIMAGEVIEW_SPINNER_TAG 862353453
static char imageWatcherKey;
static char frameWatcherKey;

- (void)zsShowSpinner:(BOOL)show {
    if (show) {
        UIActivityIndicatorView *spinnerView = (UIActivityIndicatorView *)[self viewWithTag:UIIMAGEVIEW_SPINNER_TAG];
        if (!spinnerView) {
            spinnerView = [[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge] autorelease];
            spinnerView.tag = UIIMAGEVIEW_SPINNER_TAG;
            [self addSubview:spinnerView];
            [spinnerView startAnimating];
        }

        [spinnerView setEvenCenter:self.boundsCenter];
    } else {
        [[self viewWithTag:UIIMAGEVIEW_SPINNER_TAG] removeFromSuperview];
    }
}

- (void)zsFrameChanged {
    [self zsShowSpinner:!self.image];
}

- (void)zsImageChanged {
    [self zsShowSpinner:!self.image];
}

- (BOOL)showLoadingSpinner {
    ZSPropertyWatcher *imageWatcher = (ZSPropertyWatcher *)objc_getAssociatedObject(self, &imageWatcherKey);
    return imageWatcher != nil;
}

- (void)setShowLoadingSpinner:(BOOL)aBool {
    ZSPropertyWatcher *imageWatcher = nil;
    ZSPropertyWatcher *frameWatcher = nil;

    if (aBool) {
        imageWatcher = [[[ZSPropertyWatcher alloc] initWithObject:self keyPath:@"image" delegate:self callback:@selector(zsImageChanged)] autorelease];
        frameWatcher = [[[ZSPropertyWatcher alloc] initWithObject:self keyPath:@"frame" delegate:self callback:@selector(zsFrameChanged)] autorelease];

        [self zsShowSpinner:!self.image];
    } else {
        // Remove the spinner
        [self zsShowSpinner:NO];
    }

    objc_setAssociatedObject(
        self,
        &imageWatcherKey,
        imageWatcher,
        OBJC_ASSOCIATION_RETAIN
    );

    objc_setAssociatedObject(
        self,
        &frameWatcherKey,
        frameWatcher,
        OBJC_ASSOCIATION_RETAIN
    );
}

@end

ZSPropertyWatcher.h

@interface ZSPropertyWatcher : NSObject {
    id          delegate;
    SEL         delegateCallback;

    NSObject    *observedObject;
    NSString    *keyPath;
}

@property (nonatomic, assign)   id      delegate;
@property (nonatomic, assign)   SEL     delegateCallback;

- (id)initWithObject:(NSObject *)anObject keyPath:(NSString *)aKeyPath delegate:(id)aDelegate callback:(SEL)aSelector;

@end

ZSPropertyWatcher.m

@interface ZSPropertyWatcher ()

@property (nonatomic, assign)   NSObject    *observedObject;
@property (nonatomic, copy)     NSString    *keyPath;

@end

@implementation ZSPropertyWatcher

@synthesize delegate, delegateCallback;
@synthesize observedObject, keyPath;

- (id)initWithObject:(NSObject *)anObject keyPath:(NSString *)aKeyPath delegate:(id)aDelegate callback:(SEL)aSelector {
    if (!anObject || !aKeyPath) {
        // pre-conditions
        self = nil;
        return self;
    }

    self = [super init];
    if (self) {
        observedObject = anObject;
        keyPath = aKeyPath;
        delegate = aDelegate;
        delegateCallback = aSelector;

        [observedObject addObserver:self forKeyPath:keyPath options:0 context:nil];
    }
    return self;
}

- (void)dealloc {
    [observedObject removeObserver:self forKeyPath:keyPath];

    [keyPath release];

    [super dealloc];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    [self.delegate performSelector:self.delegateCallback];
}

@end

4 ответа

Решение

Принятый ответ на этот связанный вопрос объясняет временную шкалу освобождения объектов. В результате: связанные объекты освобождаются после dealloc Метод исходного объекта закончен.

Даже больше чем твой -dealloc проблема заключается в следующем:

UIKit не является КВО-совместимым

Не было предпринято никаких усилий, чтобы сделать наблюдаемое значение ключа классов UIKit. Если какой-либо из них, это совершенно случайно и может быть нарушено по прихоти Apple. И да, я работаю на Apple в рамках UIKit.

Это означает, что вам нужно будет найти другой способ сделать это, возможно, слегка изменив компоновку вашего вида.

Что я думаю, что происходит в вашем случае это:

1) объект А получает -dealloc колл, после того как его счетчик сохранен до 0;

2) механизм ассоциации обеспечивает освобождение объекта B (который отличается от освобождения) в какой-то момент в результате.

то есть мы не знаем точно, в какой момент, но мне кажется вероятным, что такого рода семантические различия являются причиной освобождения объекта B после объекта A; объект А -dealloc селектор не может знать об ассоциации, поэтому, когда вызывается последний выпуск, -dealloc выполняется, и только после этого механизм ассоциации может отправить -release возражать B...

Посмотрите также на этот пост.

в нем также говорится:

Теперь, когда objectToBeDeallocated освобожден, objectWeWantToBeReleasedWhenThatHappens будет автоматически отправлено сообщение -release.

Надеюсь, это поможет объяснить, что вы испытываете. В остальном я ничем не могу помочь...

РЕДАКТИРОВАТЬ: просто чтобы продолжить такие интересные спекуляции после комментария DougW...

Я вижу риск возникновения циклической зависимости, если механизм ассоциации был "сломан" при освобождении объекта A (чтобы продолжить работу с вашим примером).

  1. если связанный с ассоциацией код был выполнен из метода release (вместо dealloc), для каждого релиза вы проверяли бы, имеет ли "владеющий" объект (объект A) счет удержания 1; на самом деле, в таком случае вы знаете, что уменьшение его счетчика запусков вызовет освобождение, поэтому прежде чем сделать это, вы сначала освободите связанный объект (объект B в вашем примере);

  2. но что произойдет в случае, если объект B также "владеет" третьим объектом, скажем, C? то, что произошло бы, - то, что в то время, когда выпуск вызван на объекте B, когда объект B сохраняет счет, равный 1, C будет выпущен;

  3. Теперь рассмотрим случай, когда объект C "владел" самым первым из этой последовательности, объектом A. Если при получении указанного выше релиза C имел счет сохранения 1, он сначала попытается выпустить связанный с ним объект, который это;

    1. но количество выпусков A по-прежнему равно 1, поэтому еще один выпуск будет отправлен в B, у которого еще есть счет сохранения 1; и так далее, в цикле.

Если вы, с другой стороны, отправляете релиз из -dealloc, такая циклическая зависимость не представляется возможной.

Это довольно надумано, и я не уверен, что мои рассуждения верны, поэтому не стесняйтесь комментировать это...

objc_getAssociatedObject() для OBJC_ASSOCIATION_RETAIN ассоциация возвращает автоматически освобожденный объект. Можете ли вы вызывать его раньше в той же области цикла цикла выполнения / пула автоматического освобождения, что и для объекта A, который освобожден? (Вероятно, вы можете проверить это быстро, изменив ассоциацию на NONATOMIC).

Другие вопросы по тегам