5

I need to perform an action in the dealloc method of a category. I've tried swizzling but that doesn't work (nor is it a great idea).

In case anyone asks, the answer is no, I can't use a subclass, this is specifically for a category.

I want to perform an action on delay using [NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:] or [self performSelector:withObject:afterDelay:] and cancel it on dealloc.

The first issue is that NSTimer retains the target, which I don't want. [self performSelector:withObject:afterDelay:] doesn't retain, but I need to be able to call [NSObject cancelPreviousPerformRequestsWithTarget:selector:object:] in the dealloc method or we get a crash.

Any suggestions how to do this on a category?

jscs
  • 63,694
  • 13
  • 151
  • 195
Guy Kogus
  • 7,251
  • 1
  • 27
  • 32
  • Means ? Did your implementation file not contains the dealloc method ? – Midhun MP Feb 05 '13 at 13:45
  • Seeing some code would be good, even if it does not work (or does not work yet). – Sergey Kalinichenko Feb 05 '13 at 13:46
  • 1
    You can use Matt Gallagher's ["supersequent implementation"](http://www.cocoawithlove.com/2008/03/supersequent-implementation.html). Be sure to consider the drawbacks of this approach in contrast to swizzling, though. – Nate Chandler Feb 05 '13 at 14:00
  • It sounds like you want to take some arbitrary action when an instance of a class whose implementation you don't have access to is deallocated. Swizzling `dealloc` should work, as long as you call the original method too; the category override won't because you can't access the original. If you elaborate on what the action you need to take is, somebody might be able to suggest a better way to do it. – jscs Feb 06 '13 at 01:51
  • Nate, I tried the supersequent implementation and that didn't work either. The moment I call the supersequent dealloc method the instance is freed and then instantly retained (by some ARC magic) and I get a crash. I'd rather not have to stop using ARC. – Guy Kogus Feb 10 '13 at 10:38
  • 1
    Your solution to your use case is unfortunately flawed: `dealloc` will not be called _because_ the timer retains the target. – jscs Feb 11 '13 at 19:23
  • nothing works for you ..... and only you. add some code or so – Daij-Djan Feb 13 '13 at 12:57

3 Answers3

8

I still think it would be better to subclass your class and not mess with the runtime, but if you are definitely sure you need to do it in a category, I have an option in mind for you. It still messes with the runtime, but is safer than swizzling I think.

Consider writing a helper class, say calling it DeallocHook which can be attached to any NSObject and perform an action when this NSObject gets deallocated. Then you can do something like this:

// Instead of directly messing with your class -dealloc method, attach
// the hook to your instance and do the cleanup in the callback 
[DeallocHook attachTo: yourObject 
             callback: ^{ [NSObject cancelPrevious... /* your code here */ ]; }];

You can implement the DeallocHook using objc_setAssociatedObject:

@interface DeallocHook : NSObject
@property (copy, nonatomic) dispatch_block_t callback;

+ (id) attachTo: (id) target callback: (dispatch_block_t) block;

@end

Implementation would be something like this:

#import "DeallocHook.h"
#import <objc/runtime.h>

// Address of a static global var can be used as a key
static void *kDeallocHookAssociation = &kDeallocHookAssociation;

@implementation DeallocHook

+ (id) attachTo: (id) target callback: (dispatch_block_t) block
{
    DeallocHook *hook = [[DeallocHook alloc] initWithCallback: block];

    // The trick is that associations are released when your target
    // object gets deallocated, so our DeallocHook object will get
    // deallocated right after your object
    objc_setAssociatedObject(target, kDeallocHookAssociation, hook, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    return hook;
}


- (id) initWithCallback: (dispatch_block_t) block
{
    self = [super init];

    if (self != nil)
    {
        // Here we just copy the callback for later
        self.callback = block;
    }
    return self;
}


- (void) dealloc
{
    // And we place our callback within the -dealloc method
    // of your helper class.
    if (self.callback != nil)
        dispatch_async(dispatch_get_main_queue(), self.callback);
}

@end

See Apple's documentation on Objective-C runtime for more info about the associative references (although I'd say the docs are not very detailed regarding this subject).

I've not tested this thoroughly, but it seemed to work. Just thought I'd give you another direction to look into.

Egor Chiglintsev
  • 1,252
  • 10
  • 9
  • How does this inform the class to call the block on call to `dealloc`? – Guy Kogus Feb 10 '13 at 10:51
  • he did write it above: The trick is that associations are released when your target object gets deallocated, so our DeallocHook object will get deallocated right after your object – Daij-Djan Feb 10 '13 at 10:55
  • Associations are automatically released when your object gets deleted, so if no other references to the hook object remain, it will be deallocated as well. The block is called in `-dealloc` of the hook object and that does the trick. It's just an idea and might not work in your case since I'm not sure in what order the `-dealloc` methods would get called on your object and on the hook object, but the whole scheme seems worth a try. – Egor Chiglintsev Feb 10 '13 at 10:55
  • 1
    Clever idea! Something to commit to memory, just in case. – Jasper Blues Feb 10 '13 at 11:06
4

I just stumbled on a solution to this that I haven't seen before, and seems to work...

I have a category that--as one often does--needs some state variables, so I use objc_setAssociatedObject, like this:

Memento *m = [[[Memento alloc] init] autorelease];
objc_setAssociatedObject(self, kMementoTagKey, m, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

And, I needed to know when the instances my category extending were being dealloced. In my case it's because I set observers on self, and have to remove those observers at some point, otherwise I get the NSKVODeallocateBreak leak warnings, which could lead to bad stuff.

Suddenly it dawned on me, since my associated objects were being retained (because of using OBJC_ASSOCIATION_RETAIN_NONATOMIC), they must be being released also, and therefore being dealloced...in fact I had implemented a dealloc method in the simple storage class I had created for storing my state values. And, I postulated: my associated objects must be dealloced before my category's instances are! So, I can have my associated objects notify their owners when they realize they are being dealloced! Since I already had my retained associated objects, I just had to add an owner property (which is not specified as retain!), set the owner, and then call some method on the owner in the associated object's dealloc method.

Here's a modified part of my category's .m file, with the relevant bits:

#import <objc/runtime.h> // So we can use objc_setAssociatedObject, etc.
#import "TargetClass+Category.h"

@interface TargetClass_CategoryMemento : NSObject
{
    GLfloat *_coef;
}
@property (nonatomic) GLfloat *coef;
@property (nonatomic, assign) id owner;
@end
@implementation TargetClass_CategoryMemento
-(id)init {
    if (self=[super init]) {
        _coef = (GLfloat *)malloc(sizeof(GLfloat) * 15);
    }
    return self;
};
-(void)dealloc {
    free(_coef);
    if (_owner != nil 
        && [_owner respondsToSelector:@selector(associatedObjectReportsDealloc)]) {
        [_owner associatedObjectReportsDealloc];
    }
    [super dealloc];
}
@end

@implementation TargetClass (Category)

static NSString *kMementoTagKey = @"TargetClass+Category_MementoTagKey";

-(TargetClass_CategoryMemento *)TargetClass_CategoryGetMemento
{
    TargetClass_CategoryMemento *m = objc_getAssociatedObject(self, kMementoTagKey);
    if (m) {
        return m;
    }
    // else
    m = [[[TargetClass_CategoryMemento alloc] init] autorelease];
    m.owner = self; // so we can let the owner know when we dealloc!
    objc_setAssociatedObject(self, kMementoTagKey, m,  OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return m;
}

-(void) doStuff
{
    CCSprite_BlurableMemento *m = [self CCSprite_BlurableGetMemento];
    // do stuff you needed a category for, and store state values in m
}

-(void) associatedObjectReportsDealloc
{
    NSLog(@"My associated object is being dealloced!");
    // do stuff you need to do when your category instances are dealloced!
}

@end

The pattern here I learned somewhere (probably on S.O.) uses a factory method to get or create a memento object. Now it sets the owner on the memento, and the memento's dealloc method calls back to let the owner know it's being dealloced

CAVEATS:

  • Obviously, you have to have your associated object set with OBJC_ASSOCIATION_RETAIN_NONATOMIC, or it won't be retained and released for you automatically.
  • This becomes trickier if your memento/state associated object gets dealloced under other circumstances than the owner being dealloced...but you can probably train one object or the other to ignore that event.
  • The owner property can't be declared as retain, or you'll truly create a strong reference loop and neither object will ever qualify to be dealloced!
  • I don't know that it's documented that OBJC_ASSOCIATION_RETAIN_NONATOMIC associated objects are necessarily released before the owner is completely dealloced, but it seems to happen that way and almost must be the case, intuitively at least.
  • I don't know if associatedObjectReportsDealloc will be called before or after the TargetClass's dealloc method--this could be important! If it runs afterwards, if you try to access member objects of the TargetClass you will crash! And my guess is that it's afterwards.

This is a little messy, because you're double-linking your objects, which requires you to be very careful to keep those references straight. But, it doesn't involve swizzling, or other interference with the runtime--this just relies on a certain behavior of the runtime. Seems like a handy solution if you already have an associated object. In some cases it might be worth creating one just to catch your own deallocs!

S'pht'Kr
  • 2,809
  • 1
  • 24
  • 43
  • Such a clever way around this! This is a brilliant hack that allows objc categories to become so much more powerful. Thanks for sharing! Others are recommending subclassing, and typically I would agree, however some classes are just not good candidates for subclassing (Class clusters... I'm looking your direction). – KellyHuberty Sep 19 '15 at 20:12
2

Your proposed solution unfortunately won't work: because NSTimer retains its target, the target will never run its dealloc until the timer has been invalidated. The target's retain count will always be hovering at 1 or above, waiting for the timer to release it. You have to get to the timer before dealloc. (Pre-ARC, you could override retain and release and destroy the timer, although that's really not a good solution.)

NSThread also has this problem, and the solution is simple: a bit of redesigning separates the controller of the thread from the "model". The object which creates and owns the thread, or timer in this case, should not also be the target of the timer. Then, instead of the retain cycle you currently have (timer owns object which owns timer), you have a nice straight line: controller owns timer which owns target. Outside objects only need to interact with the controller: when it is deallocated, it can shut down the timer without you having to play games with overriding dealloc or other memory management methods.

That's the best way to handle this. In the case that you can't do that for some reason -- you're talking about category overrides, so apparently you don't have the code for the class which is the target of the timer (but you can still probably make a controller even in that case) -- you can use weak references. Unfortunately I don't know any way to make an NSTimer take a weak reference to its target, but GCD will give you a fair approximation via dispatch_after(). Get a weak reference to the target and use that exclusively in the Block you pass. The Block will not retain the object through the weak reference (the way NSTimer would), and the weak reference will of course be nil if the object has been deallocated before the Block runs, so you can safely write whatever message sends you like.

Community
  • 1
  • 1
jscs
  • 63,694
  • 13
  • 151
  • 195