When does an associated object get released?
Asked Answered
H

4

33

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
Hersch answered 18/5, 2011 at 2:39 Comment(10)
Interesting question! Can you post a bit more information about why this particular setup is necessary? A bit more description than just "Object A" and "Object B" could go a long way towards helping us help you. :)Investment
@Dave DeLong - I'm running out to lunch, but I'll post some source code when I get back. Basically, I'm trying to extend UIImageView through a category to give it a property flag that will display a spinner if the image property is nil. I'd like to use a category so I don't have to subclass, and can use this anywhere. I'm trying to do this without overriding any base class methods (somewhat just for fun ;).Hersch
@Dave DeLong - Enough code for ya? ;). Only way I can think to do what I want to do is swizzle in my own dealloc method when the user turns the showLoadingSpinner flag to YES, set my associated objects to nil there (releasing them) and swizzle back in the original to call it. I've seen you around here enough to guess that you know how messy that can get though. Cheers.Hersch
couldn't you just use a UIView that embeds a UIImageView and a UIProgressIndicator? why does this have to be done directly on the UIImageView?Investment
@Dave DeLong - I use UIImageViews in a lot of different places in my application, and already extend it in some places for a variety of reasons. Doing that would require me to replace all those, and inject another layer of inheritance in some places. Point is, yes, I can do that. It would be really nice not to have to though. This is also somewhat just for fun to see if it can be done, and may not make it into production (especially if the ultimate solution involves swizzling).Hersch
Can the original poster or someone with a firm grasp of the content please edit the title of this question to reflect what it's really about - the accepted answer must satisfy the OP but it doesn't answer "When does an associated object get released?" - Thanks.Dryclean
@Dryclean - I would sooner un-accept the current answer than change the question.Hersch
#10843329 answers your question better I think as it explains the deallocation timeline - but this question and answer is probably useful to others who try to do KVO with UIKit... how about if just the title changes to something about trying to do KVO with UIKit classes?Dryclean
@jhabbot - I see your point, but that's just not my question ;). The UIKit kvo stuff is interesting, but really isn't to the point and I've un-accepted it accordingly. The answer you point out does appear to answer my question, so if you'd like to repost it here I will accept it. If people feel that the two questions are similar enough that they are duplicates, generally the other one would be closed since it was asked much later. Personally I think that although the answers are the same, the questions are different enough to both stand.Hersch
Ok, I've posted an answer - that wasn't really my intention as I think the previous answer is also relevant to the main content of your question re. KVO in UIKit, but it's got most votes so it will stay near the top and be useful still :)Dryclean
D
13

The accepted answer to this related question explains the deallocation timeline of objects. The upshot is: Associated objects are released after the dealloc method of the original object has finished.

Dryclean answered 21/8, 2012 at 2:22 Comment(1)
Thanks! I edited the link to point to the answer itself. Cheers :)Hersch
I
71

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.

Investment answered 18/5, 2011 at 21:46 Comment(14)
@Dave DeLong - This may be true, but the documentation states that NSObject supplies "Automatic" change notification for KVC compliant properties. "frame" and "image" are both KVC compliant properties, so why should we expect that they would not benefit from NSObject's automatic implementation? Is that exception documented somewhere? (references: automatic KVO tinyurl.com/6fzsyt5 and KVC compliance rules tinyurl.com/6y5k4ao). Thanks!Hersch
@Dave DeLong - Hey I'd like to accept your answer if it's true, but can you link to any kind of reference or documentation about this? It seems contrary to the documentation I referenced.Hersch
@Hersch developer.apple.com/library/mac/#documentation/General/… in the Note. By "generally" it means "if it works, cool." We don't put forward any active effort to make classes KVO-compliant.Investment
@Dave DeLong - Hmmm, well I'm still confused since the automatic NSObject KVO seems to cover basic properties like this. The intent of that notice seems to be that efforts haven't been made to introduce KVO compliance beyond the default... which is all I'm relying on. Still, good to know thanks.Hersch
@Hersch in order for KVO to work on UIKit classes, they have to be using their setter methods internally for you to get notified of changes. A lot of them don't; they just manipulate the ivar directly.Investment
@Dave DeLong: Out of interest, why is UIKit not KVO-compliant? Surely there's a good reason for it, but either I'm too simple or missing information to work it out.Librium
@Sedate why? because it isn't. that decision far predates me, and i'm not aware of all the intricacies of the pros and cons, nor would this be an appropriate place to share them. :)Investment
The addition of properties (ObjC-2.0) would have been an ideal time: "Only change -setFoo: and -foo to @property foo in the header when you're sure they're KVO-compliant".Expedient
@Expedient except that the iOS 2.0 SDK came out in 2008, which was well after Objective-C 2.0 came out, which means UIKit has always used @property syntax.Investment
KVO is a means of observing model changes; it's not really needed on UIKit (or AppKit) classes.Hunfredo
@Dave DeLong: True, I was thinking of AppKit!Expedient
@Chris Hanson: By the same logic, properties are not really needed on UIKit (or AppKit) classes. I don't really see the point of making something a property if it's not KVO-compliant.Expedient
As @Dryclean pointed out in a comment on the question, this answer does not technically answer the question at hand. Valuable information, but I have to agree, so I'll un-accept it and hope that it stands on its votes.Hersch
like using KVO on UITextfield for its property "text" wont fire adObserver method if we change its text using keyboard. But if we pragmatically sets the text of textfield like [myTf setText@"call observer"]; addObserver method will get called as we are using property setter in this case. Hope I am getting this right.Exculpate
D
13

The accepted answer to this related question explains the deallocation timeline of objects. The upshot is: Associated objects are released after the dealloc method of the original object has finished.

Dryclean answered 21/8, 2012 at 2:22 Comment(1)
Thanks! I edited the link to point to the answer itself. Cheers :)Hersch
M
3

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).

  1. 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);

  2. 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;

  3. 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;

    1. 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...

Micronesia answered 18/5, 2011 at 12:26 Comment(3)
Yes, that seems to be what I'm observing. I'm not sure you're correct that it would be impossible for the associated objects to be released before object A is deallocated though. There must be code in NSObject's dealloc method to trigger the process, so it seems that they could just as easily have placed that code into the release method at the point at which it decides it's time to deallocate. To me that seems like a bad design decision, but maybe there's a reason we can't know without the source. Anyway, thanks. Unless anyone comes up with better I'll accept and start swizzling.Hersch
@DougW: thanks... I have been intrigued by your comment, so edited my answer above adding more thoughts, if you like reviewing them...Micronesia
Hey, I see what you're saying but that's not what I'm suggesting. NSObject certainly has a point in code where it has decided to deallocate an object, but hasn't begun yet. That's the point I'm suggesting would be ideal to release associated objects. When you think about it, this is the same thing you do by overriding dealloc and releasing properties. In your example, object B would never be released, because object C's retention of object A would prevent object A from being deallocated in the first place.Hersch
E
1

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).

Expedient answered 18/7, 2011 at 19:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.