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