search
LoginSignup
1

More than 3 years have passed since last update.

posted at

updated at

忘備録-Swiftのメモリ管理

趣味でIOSアプリ開発をかじっていた自分が、改めてSwiftを勉強し始めた際に、曖昧に理解していたところや知らなかったところをメモしています。いつか書き直します。

参考文献

この記事は以下の書籍の情報を参考にして執筆しました。

リファレンスカウンタの概念

例えばDogクラスのインスタンスを参照する2つの変数があるとする。


class Animal: CustomStringConvertible{
  var weight: Int
  class var className: String {"Animal"}

  init(w: Int){
    weight = w
  }
  var description: String{
    Self.className
  }
}

class Dog: Animal {
  var name: String = ""

  init(n:String, w:Int) {
    super.init(w: w)
    name = n
  }

  override class var className: String {"Dog"}

  override var description: String{
    name
  }

  deinit {
    print("\(name) : deinit")
  }
}

// ①
var pochi: Dog! = Dog(n: "Pochi", w: 5)
print(pochi!)    // pochi
// ②
var x: Dog! = pochi    // インスタンスの参照を渡している。
x.name = "x"
// 同じインスタンスを指しているか
print(pochi === x)    // true
print(pochi!)
// ③
pochi = nil    // 何も表示されないのでdeinitは呼ばれていない
print(pochi === x)    // false
// ④
x = nil    // x : deinit // 全ての参照してる変数がなくなったので呼ばれた。

この時
①の時点では、変数pochiだけがインスタンスを参照するので、リファレンスカウンタは1。
②の時点では、変数pochiとxがインスタンスを参照するので、リファレンスカウンタは2。
③の時点では、pochiがnilを参照することになるので、リファレンスカウンタは1。
④の時点でxもnilを指すのでリファレンスカウンタが0になる。この時点で参照する変数がなくなり、インスタンスは解放される。

ARCによるメモリ管理

ARC (Automatic Reference Counting) :
リファレンス値を増減させることによってメモリを管理する。
リファレンスカウンタをいつ増加させるかはコンパイル時に決定できる。
インスタンスが不要になるタイミングがコード上で分かっているため、処理が高速でdメモリか開放に伴う不可の集中も起きにくい。

強い参照の循環

強い参照の循環(環状参照) : あるインスタンスが他のインスタンスを参照して、そのインスタンスから参照が帰ってくる、関係が環状になっているとメモリが解放されない。

A→B→C→D OK
A→B→C→A… NG
A→B→C→B… NG

class Student: CustomStringConvertible {
  let name: String
  var club: Club? = nil

  init(n: String){
    name = n
  }

  var description: String{
    "\(name): \(club?.name ?? "帰宅部")所属"
  }

  deinit {
    print("\(name) : deinit")
  }
}

class Club: CustomStringConvertible {
  let name: String
  var members = [Student]()

  init(n: String){
    name = n
  }

  func add(_ s: Student){
    members.append(s)
    s.club = self
  }

  var description: String{
    "\(name): \(members.count)人"
  }

  deinit {
    print("\(name) : deinit")
  }
}

do {
  let hoge: Club = Club(n: "hoge部")
  let taro: Student = Student(n: "taro")
  print(hoge)    // hoge部: 0人
  print(taro)    // taro: 帰宅部所属
// taro : deinit
// hoge部 : deinit
}
print("終わり")    // 終わり

addメソッドを実行して互いに参照させてみるとインスタンスの参照が残ったままになるのでdeinitが実行されていないことがわかる。

do {
  let hoge: Club = Club(n: "hoge部")
  let taro: Student = Student(n: "taro")
  hoge.add(taro)
  print(hoge)    // hoge部: 0人
  print(taro)    // taro: 帰宅部所属
}
print("終わり”””)    // 終わり

弱い参照

通常の変数や定数からクラスのインスタンスを参照すると、リファレンスカウンタの値が1増加。
弱い参照 : リファレンスカウンタの値が増えない参照方法。変数をweakというキーワードで修飾。参照していたインスタンスが解放されるとnilが代入される(ゼロ化される)ので変数かつオプショナル型で宣言しなければならない。

強い参照の循環で使用した例で適応してみる。
class StudentのClubクラスへの参照を弱い参照に変更する。
実行するとインスタンスが解放されていることが確認できた。

class Student: CustomStringConvertible {
  let name: String
  weak var club: Club? = nil    //ここを変えるだけ

  init(n: String){
    name = n
  }

  var description: String{
    "\(name): \(club?.name ?? "帰宅部")所属"
  }

  deinit {
    print("\(name) : deinit")
  }
}

//Clubクラスは変更なし//

do {
  let hoge: Club = Club(n: "hoge部")
  let taro: Student = Student(n: "taro")
  hoge.add(taro)
  print(hoge)    // hoge部: 1人
  print(taro)    // taro: hoge部所属
}
// taro : deinit
// hoge部 : deinit
print("終わり")    // 終わり

非所有参照

非所有参照 : クラスのインスタンスを参照しても所有権を主張せず、リファレンスカウンタを増やすことがない。unownedという修飾語をつける。
弱い参照と違う点
・常に何かのインスタンスを参照し続け、nilを値とはしない。
・参照してたインスタンスが解放されてもnilを代入しない。(ゼロ化しない)
・解放した後、誤ってアクセスすると実行時エラーとなるので注意が必要。
・値を自動的に変更しないので定数に指定することも可能
・ゼロ化するときはオーバヘッドになる(コストがかかる)が、ゼロ化しないので高速。

先ほど書いた弱い参照を置き換えた。
弱い参照と同様にインスタンスが解放されていることが確認できた。

class Student: CustomStringConvertible {
  let name: String
  unowned var club: Club? = nil

  init(n: String){
    name = n
  }

  var description: String{
    "\(name): \(club?.name ?? "帰宅部")所属"
  }

  deinit {
    print("\(name) : deinit")
  }
}

//Clubクラスは変更なし//

do {
  let hoge: Club = Club(n: "hoge部")
  let taro: Student = Student(n: "taro")
  hoge.add(taro)
  print(hoge)    // hoge部: 1人
  print(taro)    // taro: hoge部所属
}
// taro : deinit
// hoge部 : deinit
print("終わり")    // 終わり

例えばこのようにunwnedだけでインスタンスを参照すると、リファレンスカウンタを増やさないので、
即座にインスタンスが解放されて、その後変数を使うと落ちる。

unowned let taro: Student = Student(n: "taro")    // taro : deinit
print(taro)    //落ちる

オプショナルチェーン

例えばTeacherクラスを追加し、生徒クラスのクラブクラスの先生クラスの名前プロパティにアクセスするのには3回unwrapする必要がある。

class Student    //変更なし

class Club: CustomStringConvertible {
  let name: String
  var members = [Student]()
  weak var teacher: Teacher?    // 追加

  init(n: String){
    name = n
  }

  func add(_ s: Student){
    members.append(s)
    s.club = self
  }

  var description: String{
    "\(name): \(members.count)人"
  }

  deinit {
    print("\(name) : deinit")
  }
}

class Teacher {
  let name: String

  init(n: String){
    name = n
  }
}
let taro: Student? = nil
print(taro!.club!.teacher!.name)    // プロパティのどれかがnilならerror

これをif let で書くと記述が長くなってしまう。
しかしオプショナルチェーンを使えばコンパクトに書ける。

if let name = taro?.club?.teacher?.name {
  print(name)
}

この例ではtaroがnilではないかつclubがnilではないかつteacherがnilではないとき、中の処理が実行される。

オプショナルチェーンを使った代入

nilでなければ代入される。

do{
  let taro: Student?
  taro = Student(n: "taro")
  let hoge = Club(n: "hoge")
  hoge.add(taro!)
  let t = Teacher(n: "fuga")
  taro?.club?.teacher = t

  if let name = taro?.club?.teacher?.name {
    print(name)    // fuga
    print(type(of:name))
  }
}

nilなら代入されないのでif文が実行されない

do{
  let taro: Student?
  taro = Student(n: "taro")
  let hoge = Club(n: "hoge")
  // hoge.add(taro!)    // コメントアウトしてみる
  let t = Teacher(n: "fuga")
  taro?.club?.teacher = t

  //nilになるので実行されずに終わる
  if let name = taro?.club?.teacher?.name {
    print(name)
    print(type(of:name))
  }
}

キーパス

キーパス : インスタンスが相互に参照し合う関係があった時、あるインスタンスを起点として参照をたどって別のインスタンスを参照できる経路。

do{
  let taro: Student?
  taro = Student(n: "taro")
  let hoge = Club(n: "hoge")
  hoge.add(taro!)
  let t = Teacher(n: "fuga")
  taro?.club?.teacher = t

  let path = \Student.club?.teacher?.name    // これがキーパス

  if let name = taro![keyPath: path] {
    print(name)
    print(type(of:name))
  }
}

メモリアクセスの安全性

Swift処理系はメモリへのアクセスの安全性を高めるために、排他規則という原則に従ってコンパイル時に性的チェックを行っている。
具体的には3つの条件を満たす複数のアクセスがあった場合に問題が発生する。
1.少なくとも1つが書き込みアクセス
2.それらがメモリの同じ位置にアクセスする
3.それらの実行が重複する

メモリへのアクセスの可能性は、変数の参照、代入、inout引数を使った関数呼び出し、構造体のmutatingメソッド呼び出しがある。
inoutとmutatingは開始から終了までに別のコードが実行される可能性がある。

例としてはswap関数で同じ配列の中身を入れ替えようとするとエラーとする。

var list = [0, 1, 2, 3]
swap(&list[1], &list[2])    // error swapAtを使うように促される。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1