سؤال

I have a method that adds an operation to the serial NSOperationQueue. Since I want to call the method on a regular basis, I use dispatch source timer.

However, this method can also be called in response to user actions. When this happens (e.g. a moment before the method is called due to timer) I extend timer's fire date.

The problem is the code I've written has a retain cycle and I don't understand where.

Here is the reduced example that demonstrate the problem (don't forget to set deployment SDK to 10.7):

#import <Foundation/Foundation.h>


@interface MyObject : NSObject

@end


@implementation MyObject
{
    NSOperationQueue *_queue;
    dispatch_source_t _timer;
}

- (id)init
{
    self = [super init];

    if (self)
    {
        _queue = [[NSOperationQueue alloc] init];
        [_queue setMaxConcurrentOperationCount:1];
    }

    return self;
}

- (void)dealloc
{
    NSLog(@"dealloc");
    [_queue cancelAllOperations];
    dispatch_source_cancel(_timer);
    dispatch_release(_timer);
}

- (void)scheduleTimer
{
    if (_timer)
    {
        dispatch_source_cancel(_timer);
        dispatch_release(_timer);
    }

    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
                                    0,
                                    0,
                                    dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));

    if (_timer)
    {
        __weak MyObject *selfBlock = self;
        dispatch_source_set_event_handler(_timer, ^{
            dispatch_source_cancel(_timer);
            [selfBlock doMethod];
        });
        dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 0);
        dispatch_resume(_timer);
    }
}

- (void)doMethod
{
    NSLog(@"doMethod");

    __weak MyObject *selfBlock = self;

    [_queue cancelAllOperations];
    [_queue addOperationWithBlock:^{
        [selfBlock scheduleTimer];
    }];
}

@end


int main(int argc, const char * argv[])
{
    @autoreleasepool {

        MyObject *obj = [MyObject new];
        [obj doMethod];

        sleep(10);

        obj = nil;
        NSLog(@"something still points to obj");

        sleep(10);
    }
    return 0;
}
هل كانت مفيدة؟

المحلول

There isn't actually a retain cycle here. The issue is that something you are doing or the innards of dispatch_release() (I didn't take the time to figure it out) is sending an autorelease message instead of a release message and so the final release doesn't happen until after your autorelease block closes. If you change your main routine to the following it'll show you things working as you expect:

int main(int argc, const char * argv[])
{
    @autoreleasepool {

        MyObject *obj = nil;

        @autoreleasepool {
            obj = [MyObject new];
            [obj doMethod];

            sleep(10);

            NSLog(@"set to nil");
            obj = nil;
        }

        sleep(1);  // need this to give the background thread a chance to log

        NSLog(@"something still points to obj?");

        sleep(10);

        NSLog(@"done sleeping");
    }

    return 0;
}

I changed your other code to add some logging and clean up a few things, but commented out the changes that are just for nit picky clean coding :) and it still works fine.

#import <Foundation/Foundation.h>


@interface MyObject : NSObject

@end


@implementation MyObject
{
    NSOperationQueue *_queue;
    dispatch_source_t _timer;
}

- (id)init
{
    self = [super init];

    if (self)
    {
        _queue = [[NSOperationQueue alloc] init];
        [_queue setMaxConcurrentOperationCount:1];
    }

    return self;
}

- (void)dealloc
{
    NSLog(@"dealloc");
    [_queue cancelAllOperations];

    if ( _timer )
    {
        dispatch_source_cancel(_timer);
        dispatch_release(_timer);
        // _timer = nil;
    }
}

- (void)scheduleTimer
{
    NSLog(@"Schedule timer");

    if (_timer)
    {
        dispatch_source_cancel(_timer);
        dispatch_release(_timer);
        //        _timer = nil;
    }

    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
                                0,
                                0,
                                 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));

    if (_timer)
    {
        __weak MyObject *selfBlock = self;
        dispatch_source_set_event_handler(_timer, ^{
            dispatch_source_cancel(_timer);
            [selfBlock doMethod];
        });
        dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 0);
        dispatch_resume(_timer);
    }
}

- (void)doMethod
{
    NSLog(@"doMethod");

    __weak MyObject *selfBlock = self;

    [_queue cancelAllOperations];
    [_queue addOperationWithBlock:^{
        [selfBlock scheduleTimer];
    }];
}

@end

Here's the output I get:

2013-03-10 18:15:33.829 testtimer[35328:403] doMethod
2013-03-10 18:15:33.832 testtimer[35328:1e03] Schedule timer
2013-03-10 18:15:34.833 testtimer[35328:1e03] doMethod
2013-03-10 18:15:34.835 testtimer[35328:2203] Schedule timer
2013-03-10 18:15:35.837 testtimer[35328:1e03] doMethod
2013-03-10 18:15:35.839 testtimer[35328:1d03] Schedule timer
2013-03-10 18:15:36.839 testtimer[35328:1d03] doMethod
2013-03-10 18:15:36.841 testtimer[35328:1e03] Schedule timer
2013-03-10 18:15:37.842 testtimer[35328:1e03] doMethod
2013-03-10 18:15:37.844 testtimer[35328:1e03] Schedule timer
2013-03-10 18:15:38.846 testtimer[35328:1e03] doMethod
2013-03-10 18:15:38.848 testtimer[35328:1e03] Schedule timer
2013-03-10 18:15:39.849 testtimer[35328:1e03] doMethod
2013-03-10 18:15:39.851 testtimer[35328:1d03] Schedule timer
2013-03-10 18:15:40.851 testtimer[35328:1d03] doMethod
2013-03-10 18:15:40.853 testtimer[35328:2203] Schedule timer
2013-03-10 18:15:41.854 testtimer[35328:2203] doMethod
2013-03-10 18:15:41.856 testtimer[35328:1e03] Schedule timer
2013-03-10 18:15:42.857 testtimer[35328:1d03] doMethod
2013-03-10 18:15:42.859 testtimer[35328:1d03] Schedule timer
2013-03-10 18:15:43.831 testtimer[35328:403] set to nil
2013-03-10 18:15:43.861 testtimer[35328:1d03] doMethod
2013-03-10 18:15:43.861 testtimer[35328:1d03] dealloc
2013-03-10 18:15:44.833 testtimer[35328:403] something still points to obj?

If you take out the sleep(1); call you'll see that the "something still points to obj?" log happens before the last doMethod & dealloc log statements. I suspected this was just threading and NSLog buffering which is why I put in the sleep(1); and sure enough the behavior changed as I expected it would.

Also from the docs for dispatch_queue_create in Xcode documentation viewer it says this:

Any pending blocks submitted to a queue hold a reference to that queue, so the queue is not deallocated until all pending blocks have completed.

which makes sense and also might impact the timing of the various deallocation actions.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top