Skip to main content

When does an associated object get released?



I'm attaching object B via associative reference to object A. Object B observes some properties of object A through KVO.





The problem is that object B seems to be deallocated after object A, meaning its too late to remove itself as a KVO observer of object A. I know this because I'm getting NSKVODeallocateBreak exceptions, followed by EXEC_BAD_ACCESS crashes in object B's dealloc.





Does anyone know why object B is deallocated after object A with OBJC_ASSOCIATION_RETAIN? Do associated objects get released after deallocation? Do they get autoreleased? Does anyone know of a way to alter this behavior?





I'm trying to add some things to a class through categories, so I can't override any existing methods (including dealloc), and I don't particularly want to mess with swizzling. I need some way to de-associate and release object B before object A gets deallocated.





EDIT - Here is the code I'm trying to get working. If the associated objects were released prior to UIImageView being completely deallocated, this would all work. The only solution I'm seeing is to swizzle in my own dealloc method, and swizzle back the original in order to call up to it. That gets really messy though.





The point of the ZSPropertyWatcher class is that KVO requires a standard callback method, and I don't want to replace UIImageView's, in case it uses one itself.





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




Comments

  1. Even larger than your -dealloc issue is this:

    UIKit is not KVO-compliant

    No effort has been made to make UIKit classes key-value observable. If any of them are, it is entirely coincidental and is subject to break at Apple's whim. And yes, I work for Apple on the UIKit framework.

    This means that you're going to have to find another way to do this, probably by changing your view layouting slightly.

    ReplyDelete
  2. what i think is happening in your case is this:

    1) object A receives the -dealloc call, after its retain count has gone to 0;

    2) the association mechanism ensures that object B gets released (which is different from deallocated) at some point as a consequence.

    i.e., we don't know exactly at which point, but it seems likely to me that this kind of semantic difference is the cause of object B being deallocated after object A; object A -dealloc selector cannot be aware of the association, so when the last release on it is called, -dealloc is executed, and only after that the association mechanism can send a -release to object B...

    have also a look at this post.

    it also states:


    Now, when objectToBeDeallocated is deallocated, objectWeWantToBeReleasedWhenThatHappens will be sent a -release message automatically.


    I hope this helps explaining what you are experiencing.
    As to the rest, I cannot be of much help...

    EDIT: just to keep on with such an interesting speculation after the comment by DougW...

    I see the risk of having a sort of cyclic dependency if the association mechanism were "broken" when releasing object A (to keep going with your example).


    if the association-related code were executed from the release method (instead of dealloc), for each release you would check if the "owning" object (object A) has a retain count of 1; in fact, in such case you know that decreasing its retain count would trigger dealloc, so before doing that, you would first release the associated object (object B in your example);
    but what would happen in case object B were also at its turn "owning" a third object, say it C? what would happen is that at the time release is called on object B, when object B retain count is 1, C would be released;
    now, consider the case that object C were "owning" the very first one of this sequence, object A. if, when receiving the release above, C had a retain count of 1, it would first try and release its associated object, which is A;


    but the release count of A is still 1, so another release would be sent to B, which still has a retain count of 1; and so on, in a loop.



    If you, on the other hand, send the release from the -dealloc such cyclic dependency does not seem possible.

    It's pretty contrived and I am not sure that my reasoning is right, so feel free to comment on it...

    ReplyDelete
  3. objc_getAssociatedObject() for an OBJC_ASSOCIATION_RETAIN association returns an autoreleased object. Might you be calling it earlier in the same runloop cycle / autorelease pool scope as object A is deallocated? (You can probably test this quickly by changing the association to NONATOMIC).

    ReplyDelete

Post a Comment

Popular posts from this blog

[韓日関係] 首相含む大幅な内閣改造の可能性…早ければ来月10日ごろ=韓国

div not scrolling properly with slimScroll plugin

I am using the slimScroll plugin for jQuery by Piotr Rochala Which is a great plugin for nice scrollbars on most browsers but I am stuck because I am using it for a chat box and whenever the user appends new text to the boxit does scroll using the .scrollTop() method however the plugin's scrollbar doesnt scroll with it and when the user wants to look though the chat history it will start scrolling from near the top. I have made a quick demo of my situation http://jsfiddle.net/DY9CT/2/ Does anyone know how to solve this problem?

Why does this javascript based printing cause Safari to refresh the page?

The page I am working on has a javascript function executed to print parts of the page. For some reason, printing in Safari, causes the window to somehow update. I say somehow, because it does not really refresh as in reload the page, but rather it starts the "rendering" of the page from start, i.e. scroll to top, flash animations start from 0, and so forth. The effect is reproduced by this fiddle: http://jsfiddle.net/fYmnB/ Clicking the print button and finishing or cancelling a print in Safari causes the screen to "go white" for a sec, which in my real website manifests itself as something "like" a reload. While running print button with, let's say, Firefox, just opens and closes the print dialogue without affecting the fiddle page in any way. Is there something with my way of calling the browsers print method that causes this, or how can it be explained - and preferably, avoided? P.S.: On my real site the same occurs with Chrome. In the ex