2

我以为我已经非常了解弱引用和块,但是在尝试下面的代码片段时,有一些我不明白的东西。

方法测试1:没有保留对象一切都很好

方法test2:我不明白为什么对象似乎一直保留到方法test3结束!即使在方法test2object = nil的末尾显式设置也不会改变任何东西。

方法test3:不保留对象。为什么方法test2的行为不是这样?

作为一个附带问题,我实际上想知道弱变量是否是线程安全的?即,如果我在尝试从不同线程访问弱变量时永远不会得到任何 BAD_ACCESS 异常。

@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

测试方法

- (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];
}

上面的输出是:

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
4

1 回答 1

1

在谈到您的一些问题之前,我对您的三个测试有两个观察结果:

  1. 您的测试很复杂,因为您连续运行所有三个测试,而不是返回到运行循环,因此您的自动释放池没有被刷新(因此它使事情看起来比他们持续的时间更长通常会)。您应该一次进行一项测试,以真正了解发生了什么。如果您对某个对象的生命周期得出结论,那就不好了,而您实际上可能只是遇到了一些事实,即您没有让自动释放池被刷新。

  2. 您正在执行所有这些测试 as dispatch_async,它非常快速地启动分派块,有时比底层对象超出范围的速度更快,并且您经常将 访问weakObject作为分派块中的第一步。我建议使用dispatch_after(所以你真的让调用方法有机会让变量超出范围),所以你会更好地了解发生了什么。

您的测试是一个很好的数据点,但我认为使用dispatch_after更少的sleepForTimeInterval. 感觉就像你的测试的一些特质正在伪造一些关键行为。

无论如何你问:

方法test2:我不明白为什么对象似乎一直保留到方法test3结束!即使在方法 test2 的末尾显式设置 object = nil 也不会改变任何东西。

毫无疑问,它已落入自动释放池中,直到test方法完成后才会被耗尽。

对于我之前的观点,请尝试再做test2一次,但在访问之前让操作等待两秒钟weakObject(或摆脱所有这些sleepForTimeInterval语句并使用dispatch_after而不是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");
}

你会发现这更像你预期的那样。

方法test3:不保留对象。为什么方法 test2 的行为不是这样?

不用说,你test3是一个严重的坏消息,很容易崩溃。例如,尝试注释掉 sleep 行:

- (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");
}

令我震惊的是,它的行为weak越来越不像unsafe_unretained.

作为一个附带问题,我实际上想知道弱变量是否是线程安全的?即,如果我在尝试从不同线程访问弱变量时永远不会得到任何 BAD_ACCESS 异常。

您可以通过多种方式获取异常。如果你传递weakObject给一些要求它不是的方法nil(例如NSMutableArraymethod addObject),你会得到一个异常。如果您取消引用nil对象指针的 ivars,也可能会出现异常,例如obj->objectIvar. 例如,想象一个Object实例方法 ,doSomethingLater它使用弱引用来确保它不保留Object,但随后具有本地强引用,因此它可以取消引用 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
    });
}

因此,您通常将上述内容替换为:

- (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);
        }
    });
}

不过,说实话,为什么第一个代码示例会崩溃而第二个代码示例不能崩溃的细节比在异步编程中明智地使用对象引用很重要以及未能仔细处理这些情况这一显而易见的事实更重要可能导致异常。通常,检查weakObjectis notnil可以防止许多此类问题(有一些我不打算讨论的警告)。这在调用对象方法时不太重要(因为将任何消息发送到nil结果nil),但是当您weakObject是参数或被 ivar 取消引用时,这一点很重要。

但需要明确的是,这些都与线程安全没有任何关系。您可以通过正确处理同步来实现线程安全,例如锁定机制或通过明智地使用队列(串行队列;或具有dispatch_barrier_async写入和dispatch_sync读取的并发队列的读取器/写入器模式)。

仅仅因为您有代码在其中仔细处理对象引用以免出现异常,但这并不意味着您已经实现了线程安全。线程安全需要考虑另一层问题。

于 2013-05-11T21:05:02.160 回答