SwiftのARCとweak、delegateが呼ばれなかったりObjective-Cで返り値が0やNOになる原因

(2020-06-27)

SwiftやObjective-CはARC (Automatic Reference Counting)という仕組みでメモリを管理していて、 インスタンスへの参照カウントが0になったときにそのメモリが開放される。昔は参照カウントの増減を手動でやっていたが今はARCが自動でやってくれる。

class X {
    deinit {
        print("deinit X")
    }
}
var x: X? = X()
print("inited X")
x = nil
print("setted nil")

/*
inited X
deinit X
setted nil
*/

通常それでうまく働くが、次のように循環参照するといずれの参照カウントも0にならずメモリリークする。

protocol SomeDeleagete: AnyObject {
    func foo() -> Int
}

class A {
    var b: B
    init() {
        self.b = B()
        self.b.delegate = self
    }
    deinit {
        print("deinit A")
    }
}

extension A: SomeDeleagete {
    func foo() -> Int {
        return 100
    }
}

class B {
    var delegate: SomeDeleagete?
    
    deinit {
        print("deinit B")
    }
}

そこで通常delegateにはweakを付けて代入しても参照カウントが増えない弱参照にする。それが開放されるとnilになってアクセスしてもクラッシュしない。 @escapingされるクロージャがselfをキャプチャするときに循環参照にならないよう[weak self]にするのもよくやる。 unownedでもカウントは増えないがOptionalでなく開放されてからアクセスするとクラッシュする。

class B {
    weak var delegate: SomeDeleagete?
    
    deinit {
        print("deinit B")
    }
}

したがって次のように書くとA()は即開放されるのでその関数も呼ばれない。

var b = B()
b.delegate = A()

非同期処理が絡むと一見不可解に見える挙動を起こすことがある。次の例では非同期処理を実行する前に関数内のcのスコープを抜けてしまうのでnilが返ってしまう。

class C {
    var d: D
    init() {
        d = D()
        d.delegate = self
    }
}

extension C: SomeDeleagete {
    func foo() -> Int {
        return 100
    }
}

class D{
    weak var delegate: SomeDeleagete?
    func bar() {
        print(self.delegate?.foo()) // Optional(100)
        DispatchQueue.main.async {
            print(self.delegate?.foo()) // nil
        }
    }
}

func f() {
    var c = C()
    c.d.bar()
}
f()

さらに、Objective-Cではnil(id=0x0)にメッセージを送ると0やNO、nilを返す仕様のためにより分かりづらくなっている。 特にBOOLを返すdelegateの値で分岐したりする場合は注意が必要だ。

override func f() {
    var hoge = Hoge()
    var a = A()
    hoge.delegate = a
    hoge.fuga()
}

class A: NSObject, HogeDelegate {
    func foo() -> Bool {
        return true
    }
}
@protocol HogeDelegate <NSObject>
- (BOOL)foo;
@end

@interface Hoge: NSObject

@property (weak, nonatomic) id <HogeDelegate> delegate;
- (void)fuga;
@end
@implementation Hoge

- (void)fuga {
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog([self.delegate foo] ? @"YES" : @"NO"); // NO
    });
}

@end