【问题标题】:Self deallocs after cancelPreviousPerformRequestsWithTarget在 cancelPreviousPerformRequestsWithTarget 后自行解除分配
【发布时间】:2013-02-28 21:45:16
【问题描述】:

使用 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,它会执行一些工作(即记录),然后如果 _cancel 为假,则在 0.25 秒后调用自身。同时,我安排在 1.0 秒后触发通知(最终将 _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

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

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

【问题讨论】:

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


    【解决方案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 有一个强引用,如果每个方法都必须保留/释放 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));
    }
    

    这样,至少,您可以确定在方法结束之前不会被释放。

    【讨论】:

    • 我不同意这是 NSNotificationCenter 的问题。并且做[objectToCall retain]; [objectToCall performSelector:...]; [objectToCall release]; 是不好的风格,没有任何意义。只有当我们需要保留它以供使用时,才应该保留一些东西。在[objectToCall performSelector:...];中,调用者将objectToCall交给了消息传递机制,不再使用它,所以这个函数不必保留它。相反,它是 clearReference 方法在导致其被释放后使用 self。那就是需要保留它的功能。
    • 手动保留或释放时,如何通过ARC实现? ARC 中是否允许使用上述核心基础类?
    • @TReddy 是的,您可以在 ARC 中调用 CFRetain/CFRelease。这就是为什么它们不是被禁止的[自我保留]和[自我释放]。再次,它有点难看。
    • @newacct 因为objectToCall 是一个非强引用,如果 NSNotificationCenter 是用 ARC 编译的,它会做与我建议的完全相同的 retain/performSelector:/release dance。 (因为编译器会将对象存储在 performSelector: 之前的临时强引用中,这会使 ARC 插入保留和释放。)clearReference 不可能是问题的根源,因为它无法知道 @ 987654338@ 是对 self 的最后一个引用。
    • 您在 NSNotificaitonCenter 周围的 cmets 帮助我找到了与我的情况几乎相同的链接:stackoverflow.com/questions/13744970/…
    【解决方案2】:

    __weak typeof(self)(weakSelf) = self;而不是retain-release之舞:

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

    我更喜欢这样的 ARC 方式:

    __weak typeof (self) (weakSelf) = self;
    [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(foo) object:nil];
    [weakSelf bar];
    

    如果第 2 行进行自我释放,则第 3 行的 weakSelf 将是 nil 而不是僵尸指针,并且 [nil bar] 是安全的。

    而weakSelf解决方案有2个优点:

    1。 在retain-release dance方案中,[self bar]会消耗CPU时间,没有任何意义。
    2. 弱点看起来比将对象转换为CFType并手动调用retain和release更漂亮。

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

    【讨论】:

      猜你喜欢
      • 2013-10-09
      • 2020-07-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多