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が false の場合は自身を呼び出します。その間、1.0 秒後に発生するように通知をスケジュールします (最終的には_canceltrue に設定されます)。それから私はゼロアウトし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 ] 私は生きている
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] 男: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 Tests [ 11946
: c07 ] 私は死んだ

メソッド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;と、 in が私たちへの最後の強い参照になるため、 を呼び出す前に割り当てが解除されるため[self logMe];です。しかし、なぜ ARC は私たちを引き留めていないのでしょうか?

通常、メソッドの呼び出し元が自分自身への強い参照を持っていると想定するのは安全であり、すべてのメソッドが自分自身を保持/解放する必要がある場合、それは多くのオーバーヘッドになります。(ARC でコンパイルされたコードの場合、オブジェクトのメソッドを呼び出すには、まずそのオブジェクトへの参照が必要になるため、この仮定は事実上常に当てはまります。) 残念ながら、NSNotificationCenter は、メソッドを呼び出す前にオブジェクトを保持しません。私はこれをバグと呼んでいます: ARC 以外のコードでは、通常、不明なコールバックを呼び出す前に、オブジェクトへの少なくとも一時的な強い参照があることを確認するのが丁寧です:

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

このようなコードにより、表示されているクラッシュが発生しないことが保証されます。明らかに、NSNotificationCenter はこれを行っていません。これは、Zombies インストルメントでオブジェクトの保持履歴を確認することで確認できます。

NSNotificationCenter を変更することはできないため、割り当てが解除される可能性があり、呼び出し元があなたへの強い参照を保持していない可能性がある場合に、私が以前に使用した明らかに醜い回避策の 1 つは、次のようなものです。

- (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;保持と解放のダンスではなく:

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. リテインリリース ダンス ソリューションでは、[セルフ バー] は CPU 時間を消費し、意味がありません。
2. オブジェクトを CFType に変換し、retain と release を手動で呼び出すよりも、弱点が美しく見えます。

ps
他の xxxxx ダンスと同様に、weakSelf ソリューションを「哲学ダンス」と名付けたいと思います:)

于 2016-01-07T02:36:13.527 に答える