在cancelPreviousPerformRequestsWithTarget之后自我deallocs

时间:2013-02-28 21:45:17

标签: objective-c automatic-ref-counting selector

使用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.25秒后调用自身,如果_cancel为假。同时,我计划在1.0s之后触发通知(最终将_cancel设置为true)。然后我把blah弄清楚。

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

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

  

2013-02-28 15:30:55.518测试[11946:c07] ATest / testBlah
  2013-02-28 15:30:56.789测试[11946:c07]重新运行:ATest / testBlah
  2013-02-28 15:30:56.790测试[11946:c07]我还活着   2013-02-28 15:30:56.790测试[11946:c07]老兄:0
  2013-02-28 15:30:56.791测试[11946:c07]预定
  2013-02-28 15:30:56.791测试[11946:c07] logme
  2013-02-28 15:30:56.792测试[11946:c07] ATest /testBlah✔0.00s
  2013-02-28 15:30:56.991测试[11946:c07]老兄:0
  2013-02-28 15:30:56.992测试[11946:c07]预定
  2013-02-28 15:30:56.992测试[11946:c07] logme
  2013-02-28 15:30:57.193测试[11946:c07]老兄:0
  2013-02-28 15:30:57.194测试[11946:c07]预定
  2013-02-28 15:30:57.194测试[11946:c07] logme
  2013-02-28 15:30:57.395测试[11946:c07]老兄:0
  2013-02-28 15:30:57.395测试[11946:c07]预定
  2013-02-28 15:30:57.396测试[11946:c07] logme
  2013-02-28 15:30:57.596测试[11946:c07]老兄:0
  2013-02-28 15:30:57.597测试[11946:c07]预定
  2013-02-28 15:30:57.597测试[11946:c07] logme
  2013-02-28 15:30:57.792测试[11946:c07]重置
  2013-02-28 15:30:57.793测试[11946:c07]我已经死了   2013-02-28 15:30:57.793测试[11946:c07] * - [MyClass doSomething]:消息发送到解除分配的实例0xb584880

为什么在self方法中调用cancelPreviousPerformRequestsWithTarget:后,reset被解除分配?

此问题是ARC问题还是编码错误?

2 个答案:

答案 0 :(得分:1)

整洁的问题。我会称之为NSNotificationCenter中的一个错误。这是具有相同行为的代码的简化版本。我们所做的就是让自己听取通知,并通过一个强大的(静态)参考来保持自己的生命。通知结束后,我们清除该引用。 (在您的情况下,对象的最后一个强引用是在performSelector:机制中;保留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));
}

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

答案 1 :(得分:1)

__弱类型(自我)(weakSelf)=自我;而不是保留释放舞蹈:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.tableView.sectionIndexBackgroundColor = [UIColor clearColor]; //Replace it with your desired color
}

我更喜欢这样的ARC方式:

CFRetain((__bridge CFTypeRef)(self));
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(foo) object:nil];
[self bar];
CFRelease((__bridge CFTypeRef)(self));

如果第2行自行解除分配,那么第3行的weakSelf将为nil而不是僵尸指针,[nil bar]是安全的。

weakSelf解决方案有两个优点:

1。 在保留释放舞蹈解决方案中,[自我吧]花费了CPU时间,并没有任何意义 2。 弱点看起来比将对象转换为CFType并手动调用retain和release。

P.S。
像任何其他xxxxx舞蹈一样,我想将weakSelf解决方案命名为“哲学舞蹈”。 :)