简体   繁体   中英

ARC blocks, weak and retain count

I thought that I had quite understood weak references and blocks, however when trying the below code snippets, there are a few things that I don't understand.

Method test1 : all fine the object is not retained

Method test2 : I don't understand why the object seems to get retained until the end of method test3 ! Even explicitly setting object = nil at the end of method test2 does not change anything.

Method test3 : the object is not retained. Why is method test2 not behaving like this?

As a side question, I was actually wondering if weak variables are thread safe? ie if I will never get any BAD_ACCESS exception when trying to access a weak variable from different threads.

@interface Object : NSObject
@property (nonatomic) NSInteger index;
@end

@implementation Object

- (id)initWithIndex:(NSInteger) index {
    if (self = [super init]) {
        _index = index;
    }
    return self;
}

- (void)dealloc {
    NSLog(@"Deallocating object %d", _index);
}

@end

Test methods

- (void) test1 {
    NSLog(@"test1");
    Object* object = [[Object alloc] initWithIndex:1];
    NSLog(@"Object: %@", object);
    __weak Object* weakObject = object;
    dispatch_async(dispatch_queue_create(NULL, NULL), ^{
        //NSLog(@"Weak object: %@", weakObject);
        [NSThread sleepForTimeInterval:2];
        NSLog(@"Exiting dispatch");
    });
    [NSThread sleepForTimeInterval:1];
    NSLog(@"Exiting method");
}

- (void) test2 {
    NSLog(@"test2");
    Object* object = [[Object alloc] initWithIndex:2];
    NSLog(@"Object: %@", object);
    __weak Object* weakObject = object;
    dispatch_async(dispatch_queue_create(NULL, NULL), ^{
        NSLog(@"Weak object: %@", weakObject);
        [NSThread sleepForTimeInterval:2];
        NSLog(@"Exiting dispatch");
    });
    [NSThread sleepForTimeInterval:1];
    NSLog(@"Exiting method");
}

- (void) test3 {
    NSLog(@"test3");
    Object* object = [[Object alloc] initWithIndex:3];
    NSLog(@"Object: %@", object);
    NSValue *weakObject = [NSValue valueWithNonretainedObject:object];
    dispatch_async(dispatch_queue_create(NULL, NULL), ^{
        NSLog(@"Weak object: %@", [weakObject nonretainedObjectValue]);
        [NSThread sleepForTimeInterval:2];
        NSLog(@"Exiting dispatch");
    });
    [NSThread sleepForTimeInterval:1];
    NSLog(@"Exiting method");
}

- (void) test {
    [self test1];
    [NSThread sleepForTimeInterval:3];
    [self test2];
    [NSThread sleepForTimeInterval:3];
    [self test3];
}

The output of the above is:

2013-05-11 19:09:56.753 test[1628:c07] test1
2013-05-11 19:09:56.754 test[1628:c07] Object: <Object: 0x7565940>
2013-05-11 19:09:57.755 test[1628:c07] Exiting method
2013-05-11 19:09:57.756 test[1628:c07] Deallocating object 1
2013-05-11 19:09:58.759 test[1628:1503] Exiting dispatch
2013-05-11 19:10:00.758 test[1628:c07] test2
2013-05-11 19:10:00.758 test[1628:c07] Object: <Object: 0x71c8260>
2013-05-11 19:10:00.759 test[1628:1503] Weak object: <Object: 0x71c8260>
2013-05-11 19:10:01.760 test[1628:c07] Exiting method
2013-05-11 19:10:02.760 test[1628:1503] Exiting dispatch
2013-05-11 19:10:04.761 test[1628:c07] test3
2013-05-11 19:10:04.762 test[1628:c07] Object: <Object: 0x71825f0>
2013-05-11 19:10:04.763 test[1628:1503] Weak object: <Object: 0x71825f0>
2013-05-11 19:10:05.764 test[1628:c07] Exiting method
2013-05-11 19:10:05.764 test[1628:c07] Deallocating object 3
2013-05-11 19:10:05.767 test[1628:c07] Deallocating object 2
2013-05-11 19:10:06.764 test[1628:1503] Exiting dispatch

I have two observations on your three tests before I touch on a few of your questions:

  1. Your testing is complicated by the fact that you're running all three tests in a row, not yielding back to the run loop, and thus your autorelease pool is not getting flushed (so it making things look like they're persisting longer than they normally would). You should do you testing, one test at a time, to really understand what's going on. It's not good if you're drawing conclusions about the lifespan of some object, whereas you really may just be experiencing some artifact of the fact that you're not letting the autorelease pool from being flushed.

  2. You are doing all of these tests as dispatch_async , which starts the dispatched block very quickly, sometimes more quickly than it takes the underlying object to fall out of scope and you're often accessing the weakObject as one of the first steps in the dispatched block. I'd suggest using dispatch_after (so you're really giving the calling method a chance to let the variables fall out of scope), so you'll better see what's going on.

Your tests are a good data point, but I think it's useful to also test the same stuff using dispatch_after and do one test at a time with fewer of those sleepForTimeInterval . It feels like some of the idiosyncrasies of your tests are counterfeiting some key behavior.

Anyway you ask:

Method test2: I don't understand why the object seems to get retained until the end of method test3! Even explicitly setting object = nil at the end of method test2 does not change anything.

It's undoubtedly fallen into the autorelease pool, which won't be drained until the test method is done.

To my prior points, try doing test2 again, but have the operation wait two seconds before accessing the weakObject (or get rid of all of these sleepForTimeInterval statements and use dispatch_after instead of dispatch_sync ):

- (void) test2 {
    NSLog(@"test2");
    Object* object = [[Object alloc] initWithIndex:2];
    NSLog(@"Object: %@", object);
    __weak Object* weakObject = object;
    dispatch_async(dispatch_queue_create(NULL, NULL), ^{
        [NSThread sleepForTimeInterval:2];      // new sleep
        NSLog(@"Weak object: %@", weakObject);
        [NSThread sleepForTimeInterval:2];
        NSLog(@"Exiting dispatch");
    });
    // [NSThread sleepForTimeInterval:1];       // not really necessary
    NSLog(@"Exiting method");
}

You'll see that this behaves more like you expected.

Method test3: the object is not retained. Why is method test2 not behaving like this?

Needless to say, your test3 is seriously bad news, easily crashing itself. For example, try commenting out the sleep line:

- (void) test3 {
    NSLog(@"test3");
    Object* object = [[Object alloc] initWithIndex:3];
    NSLog(@"Object: %@", object);
    NSValue *weakObject = [NSValue valueWithNonretainedObject:object];
    dispatch_async(dispatch_queue_create(NULL, NULL), ^{
        NSLog(@"Weak object: %@", [weakObject nonretainedObjectValue]);
        [NSThread sleepForTimeInterval:2];
        NSLog(@"Exiting dispatch");
    });
//    [NSThread sleepForTimeInterval:1];
    NSLog(@"Exiting method");
}

It strikes me that it's behaving less like weak and more like unsafe_unretained .

As a side question, I was actually wondering if weak variables are thread safe? ie if I will never get any BAD_ACCESS exception when trying to access a weak variable from different threads.

You can get exceptions in a lot of ways. If you pass weakObject to some method that requires that it not be nil (eg NSMutableArray method addObject ), you'll get an exception. You can also get exceptions if you dereference ivars for a nil object pointer, eg obj->objectIvar . For example, imagine a Object instance method, doSomethingLater , which uses a weak reference to ensure that it doesn't retain the Object , but then has a local strong reference so it can dereference the ivar:

- (void)doSomethingLater
{
    __weak Object *weakSelf = self;

    double delayInSeconds = 10.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        Object *strongSelf = weakSelf;
        NSLog(@"%d", strongSelf->_index); // **BAD** - this can crash of `self` has been released by this point
    });
}

Thus, you usually replace the above with:

- (void)doSomethingLater
{
    __weak Object *weakSelf = self;

    double delayInSeconds = 10.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        Object *strongSelf = weakSelf;
        if (strongSelf) {
            NSLog(@"%d", strongSelf->_index);
        }
    });
}

To be perfectly honest, though, the details of why that first code sample can crash and the second can't is less important than the obvious fact that judicious use of your object references in asynchronous programming is important, and failure to handle the situations carefully can result in exceptions. Frequently, checking that the weakObject is not nil can prevent many of these sorts of issues (with some caveats that I'm not going to go into). This is less important when calling object methods (because sending any message to nil results in nil ), but it is important when your weakObject is a parameter or being dereferenced for an ivar.

But to be clear, none of that really has any bearing on thread-safety, though. You achieve thread safety through proper handling of synchronization, such as locking mechanisms or through judicious use of queues (either serial queue; or the reader/writer pattern of a concurrent queue with dispatch_barrier_async for writes and dispatch_sync for reads).

Just because you have code where you're handling object references carefully so you don't get exceptions, doesn't mean you've achieved thread-safety. There's a whole other layer of concerns that thread-safety entails.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM