4

使用 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.0 秒后触发通知(最终设置_cancel为 true)。然后我nil了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] dude: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 问题还是编码错误?

4

2 回答 2

1

整洁的问题。我将其称为 NSNotificationCenter 中的错误。这是具有相同行为的代码的简化版本。我们所做的只是让自己听一个通知,并通过一个强(静态)引用让自己保持活力。当通知消失时,我们会清除该引用。(在您的情况下,对您的对象的最后一个强引用是在performSelector:机器中; a 的目标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));
}

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

于 2013-02-28T22:50:23.197 回答
1

__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看起来漂亮。

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

于 2016-01-07T02:36:13.527 回答