objective-c – cancelPreviousPerformRequestsWithTarget之后的self deallocs

使用ARC和iOS 6.1,我在这里有一个简单的类来演示我的问题:

#import <GHUnitIOS/GHUnit.h>

@interface MyClass : NSObject
@property BOOL cancel;
@property BOOL dead;
-(void)doSomething;
-(void)reset;
-(void)logMe;
@end

@implementation MyClass

-(id)init {
    self = [super init];
    if(self) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reset) name:@"dude" object:nil];
        NSLog(@"I'm alive");
    }
    return self;
}

-(void)dealloc {
    _dead = YES;
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [MyClass cancelPreviousPerformRequestsWithTarget:self];
    NSLog(@"I'm dead");
}

-(void)doSomething {
    NSLog(@"dude:%d", _dead);
    if(!_cancel) {
        [self performSelector:@selector(doSomething) withObject:nil afterDelay:0.2];
        NSLog(@"scheduled");
    }
    [self logMe];
}

-(void)reset {
    NSLog(@"reset");
    [MyClass cancelPreviousPerformRequestsWithTarget:self];
    _cancel = YES;
    [self doSomething];
}

-(void)logMe {
    NSLog(@"logme");
}
@end

@interface ATest : GHTestCase
@end

@implementation ATest

-(BOOL)shouldRunOnMainThread {return YES;}
-(void)setUpClass {}
-(void)tearDownClass {}
-(void)setUp {}
-(void)tearDown {}

-(void)testBlah {
    MyClass* blah = [[MyClass alloc] init];
    [blah doSomething];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^(void){
        [[NSNotificationCenter defaultCenter] postNotificationName:@"dude" object:nil];
    });
    blah = nil;
}

@end

在测试中,MyClass被实例化,我启动了doSomething,它执行一些工作(即记录),然后在0.25s后调用自身,如果_cancel为false.同时,我在1.0s之后安排了一个通知(最终将_cancel设置为true).然后我就出来了.

所以我的期望是performSelector创建的计时器:withObject:withDelay拥有对MyClass的引用.

但是,当我在启用僵尸的情况下运行此测试时,我得到了这个输出:

2013-02-28 15:30:55.518 Tests[11946:c07] ATest/testBlah
2013-02-28 15:30:56.789 Tests[11946:c07] Re-running: ATest/testBlah
2013-02-28 15:30:56.790 Tests[11946:c07] I’m alive
2013-02-28 15:30:56.790 Tests[11946:c07] dude:0
2013-02-28 15:30:56.791 Tests[11946:c07] scheduled
2013-02-28 15:30:56.791 Tests[11946:c07] logme
2013-02-28 15:30:56.792 Tests[11946:c07] ATest/testBlah ✔ 0.00s
2013-02-28 15:30:56.991 Tests[11946:c07] dude:0
2013-02-28 15:30:56.992 Tests[11946:c07] scheduled
2013-02-28 15:30:56.992 Tests[11946:c07] logme
2013-02-28 15:30:57.193 Tests[11946:c07] dude:0
2013-02-28 15:30:57.194 Tests[11946:c07] scheduled
2013-02-28 15:30:57.194 Tests[11946:c07] logme
2013-02-28 15:30:57.395 Tests[11946:c07] dude:0
2013-02-28 15:30:57.395 Tests[11946:c07] scheduled
2013-02-28 15:30:57.396 Tests[11946:c07] logme
2013-02-28 15:30:57.596 Tests[11946:c07] dude:0
2013-02-28 15:30:57.597 Tests[11946:c07] scheduled
2013-02-28 15:30:57.597 Tests[11946:c07] logme
2013-02-28 15:30:57.792 Tests[11946:c07] reset
2013-02-28 15:30:57.793 Tests[11946:c07] I’m dead
2013-02-28 15:30:57.793 Tests[11946:c07] * -[MyClass doSomething]: message sent to deallocated instance 0xb584880

在调用cancelPreviousPerformRequestsWithTarget之后,为什么self被释放:在reset方法中?

这个问题是ARC问题还是编码错误?

最佳答案 整洁的问题.我会称之为NSNotificationCenter中的一个错误.这是具有相同行为的代码的简化版本.我们所做的就是让自己听取通知,并通过一个强大的(静态)参考来保持自己的生命.通知结束后,我们清除该引用. (在您的情况下,对象的最后一个强引用是在performSelector:machinery中; performSelector的目标:保留,当您取消它时,它会释放它对您的引用.)

@interface MyClass : NSObject
@end

static MyClass *instance;

@implementation MyClass

-(id)init {
    self = [super init];
    if(self) {
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(clearReference) name:@"dude" object:nil];
        NSLog(@"I'm alive");
        instance = self;
    }
    return self;
}

- (void)clearReference {
    instance = nil;
    [self logMe];
}

-(void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    NSLog(@"I'm dead");
}

-(void)logMe {
    NSLog(@"logme");
}

@end

// Test case
[[MyClass alloc] init];
[[NSNotificationCenter defaultCenter] postNotificationName:@"dude" object:nil];

这会在[self logMe]中导致僵尸消息.原因是在clearReference中,当我们执行instance = nil时;这是对我们的最后一个强引用,所以在我们调用[self logMe]之前我们被释放了.但是,你可能会问,为什么ARC不支持我们呢?

好吧,ARC永远不会保留自我,因为假设方法的调用者对self有强烈的引用通常是安全的,并且如果每个方法都必须保留/释放self,那么会增加很多开销. (对于在ARC下编译的代码,这个假设几乎总是如此,因为要在对象上调用方法,首先需要对它进行引用.)不幸的是,NSNotificationCenter在调用方法之前不会保留您的对象.我称之为一个错误:在非ARC代码中,通常礼貌地确保在调用对象之前至少有一个临时强引用对象:

id objectToCall = ...;
[objectToCall retain];
[objectToCall performSelector:...]; // the actual callback
[objectToCall release];

这样的代码可以确保您看到的崩溃不会发生.显然,NSNotificationCenter没有这样做.您可以通过在Zombies工具中查看对象的保留历史来验证这一点.

既然你无法改变NSNotificationCenter,那么我之前使用过的一个难以理解的解决方法就是你可以解除分配并且你的调用者可能没有对你强有力的引用是这样的:

- (void)clearReference {
    CFRetain((__bridge CFTypeRef)(self));
    instance = nil;
    [self logMe];
    CFRelease((__bridge CFTypeRef)(self));
}

这样,至少,您确信在方法结束之前不会取消分配.

点赞