iOS
Swift

Swiftのメモリ管理について調べてみた

Swiftのメモリ管理、循環参照、weakキーワードあたりについて
ざっくりとした知識しかなかったので改めて調べてみました。

リファレンスカウンタについて

Swiftではクラスのインスタンスとクロージャが参照型のデータです。
参照型のインスタンスは、変数や定数に代入した際にはそのコピーが作成されるのではなく、
インスタンスを参照するための情報(ポインタ)が渡されます。

最初にインスタンスを生成した時点ではリファレンスカウンタは1であり、
別の変数(x)に作成したインスタンスを代入するとリファレンスカウンタは2になります。
また、xにnilを代入すると参照されなくなるため、リファレンスカウンタが1減ります。

class Member {
    let name: String
    init(name: String) {
        self.name = name
    }
}

var tanaka: Member? = Member(name: "Tanaka") // リファレンスカウンタは1
var x: Member? = tanaka // 2箇所から参照されているのでリファレンスカンタは2
x = nil // 参照されなくなるのでリファレンスカウンタは1
tanaka = nil // リファレンスカンタは0

リファレンスカウンタが0になり、どこからも参照されていない状態になると、
インスタンスに割り当てられていたメモリは開放されます。

ARCによるメモリ管理

上の例のように、Swiftでは、インスタンスが参照されたり
されなくなったりしたタイミングでリファレンスカウンタの増減を行い、
メモリ管理をしています。

そしてリファレンスカウンタの増減をいつ行うのかについては、
コンパイル時に決定されています。
この仕組みをARC(Automatic Reference Counting)と呼びます。

ARCのおかげでメモリ管理についてあまり意識せずにコーディングできるんですね。
そういえば大昔にObjective-Cを初めて触ったときは自分でメモリ解放とか
やっていましたね。

インスタンスが開放できないときとは?

Swiftではクラスのインスタンスはデフォルトで強参照です。
関連する複数のインスタンス同士が互いに強参照をしていると、
いつまでたっても参照カウンタが0にならず、メモリを開放できません。
これを循環参照といいます。

以下は循環参照の例です。
このコードでは、MemberクラスとTeamクラスが互いに強参照しているため、
双方のdeinitが呼ばれません。

class Member {
    let name: String
    var team: Team?

    init(name: String) {
        self.name = name
    }

    deinit {
        print("Member: \(self.name) - deinit")
    }
}

class Team {
    let name: String
    var members = [Member]()

    init(name: String) {
        self.name = name
    }

    func add(_ member: Member) {
        self.members.append(member)
        member.team = self
    }

    deinit {
        print("Team: \(self.name) - deinit")
    }
}

do {
    let t = Team(name: "Team-A")
    let m = Member(name: "Tanaka")
    t.add(m)
}

弱い参照

通常の変数や定数からクラスのインスタンスを参照するとリファレンスカウンタが
1増加しますが、変数にweakというキーワードを付けるとその変数は弱い参照となり、
リファレンスカウンタの値に影響を与えません。

weakキーワードを付けると、その変数が参照していたインスタンスが開放されたら
弱い参照の変数には自動的にnilが代入されます。
そういった理由から、weakキーワードを付ける変数はオプショナル型である必要が
あります。

weak var m: Member? = nil
do {
    let tanaka = Member(name: "Tanaka")
    m = tanaka
}
print(m) // => nil

先程のMember,Teamクラスの例では、Memberクラスを下記のように修正することで
deinitが適切に呼ばれるようになります。

class Member {
  let name: String
  weak var team: Team? // weakで修飾

  init(name: String) {
    self.name = name
  }

  deinit {
    print("Member: \(self.name)" - deinit)
  }
}

非所有参照

非所有参照は、弱い参照とは別の、循環参照を防ぐための仕組みです。
変数にunownedキーワードを付けることで非所有参照となり、
弱い参照と同様にリファレンスカウンタの値を増やしません。

弱い参照との違いは、常に何かのインスタンスを参照し続け、
nilを値とせず、参照先のインスタンスが開放されてもnilを代入しないところです。
なので、参照先のインスタンスが開放されても参照を持ち続けることがあり、
その状況で誤って参照先にアクセスするとエラーになります。

unowned var m: Member
do {
  let tanaka = Member(name: "Tanaka")
  m = tanaka
}
print(m) // => エラーが発生する

Appleのドキュメントではプログラムの実行中に変数の値がnilにならないことが明らかな場合、
弱い参照の代わりに非所有参照を使うことが推奨されているそうです。

弱い参照は、参照先のインスタンスが開放された場合に変数にnilを代入する処理が
オーバーヘッドになるため、非所有参照を使ったほうが処理を高速化できるとのことです。

最後に

unowned知らなかったので積極的に使っていきたいですね。

参考

詳解Swift 第3版