Swift自動参照カウント(ARC)

Swiftは自動参照カウント(ARC)というメカニズムを使用して、アプリケーションのメモリを追跡および管理します。

通常、手動でメモリを解放する必要はありません。ARCがクラスのインスタンスが使用されなくなったときに、その占有メモリを自動的に解放するからです。

しかし、場合によってはコード内でメモリ管理を実装する必要があります。

ARCの機能

  • init() メソッドを使用してクラスの新しいインスタンスを作成するたびに、ARCはインスタンスの情報を格納するために大きなメモリブロックを割り当てます。

  • メモリには、インスタンスの型情報と、そのインスタンスのすべての関連プロパティの値が含まれます。

  • インスタンスが使用されなくなると、ARCはインスタンスが占有していたメモリを解放し、解放されたメモリを他の用途に使用できるようにします。

  • 使用中のインスタンスが破棄されないようにするため、ARCは各インスタンスがいくつのプロパティ、定数、変数によって参照されているかを追跡および計算します。

  • インスタンスがプロパティ、定数、または変数に代入されると、それらはこのインスタンスへの強参照を作成します。強参照が存在する限り、インスタンスは破棄されません。

ARCのインスタンス

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) 开始初始化")
    }
    deinit {
        print("\(name) 被析构")
    }
}

// 值会被自动初始化为nil,目前还不会引用到Person类的实例
var reference1: Person?
var reference2: Person?
var reference3: Person?

// 创建Person类的新实例
reference1 = Person(name: "サンプル")


//赋值给其他两个变量,该实例又会多出两个强引用
reference2 = reference1
reference3 = reference1

//断开第一个强引用
reference1 = nil
//断开第二个强引用
reference2 = nil
//断开第三个强引用,并调用析构函数
reference3 = nil

上記のプログラムを実行すると、出力結果は次のようになります:

サンプル 开始初始化
サンプル 被析构

クラスインスタンス間の循環強参照

上記の例では、ARCは新しく作成されたPersonインスタンスの参照数を追跡し、Personインスタンスが不要になったときにそれを破棄します。

しかし、クラスが決して強参照0にならないようなコードを書くことがあります。これは、2つのクラスインスタンスが互いに強参照を保持し、互いを破棄されないようにする場合に発生します。これが循環強参照と呼ばれるものです。

インスタンス

以下は、意図せず循環強参照が発生する例です。この例では、アパートとその住民をモデル化するために、PersonとApartmentという2つのクラスを定義しています:

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) 被析构") }
}

class Apartment {
    let number: Int
    init(number: Int) { self.number = number }
    var tenant: Person?
    deinit { print("Apartment #\(number) 被析构") }
}

// 两个变量都被初始化为nil
var サンプル: Person?
var number73: Apartment?

// 赋值
サンプル = Person(name: "サンプル")
number73 = Apartment(number: 73)

// 意感叹号是用来展开和访问可选变量 サンプル 和 number73 中的实例
// 循环强引用被创建
サンプル!.apartment = number73
number73!.tenant = サンプル

// 断开 サンプル 和 number73 变量所持有的强引用时,引用计数并不会降为 0,实例也不会被 ARC 销毁
// 注意,当你把这两个变量设为nil时,没有任何一个析构函数被调用。
// 强引用循环阻止了Person和Apartment类实例的销毁,并在你的应用程序中造成了内存泄漏
サンプル = nil
number73 = nil

インスタンス間の循環強参照を解決する

Swiftは、クラスのプロパティを使用する際に発生する循環強参照の問題を解決するための2つの方法を提供します:

  • 弱参照
  • 非所有参照

弱参照と非所有参照を使用すると、循環参照内の一方のインスタンスが強参照を保持せずに別のインスタンスを参照できます。これにより、インスタンスは循環強参照を発生させずに互いに参照できます。

ライフサイクル中にnilになる可能性があるインスタンスには弱参照を使用します。逆に、初期化で値を割り当てた後は決してnilに再代入されないインスタンスには非所有参照を使用します。

弱参照のインスタンス

class Module {
    let name: String
    init(name: String) { self.name = name }
    var sub: SubModule?
    deinit { print("\(name) 主模块") }
}

class SubModule {
    let number: Int
    
    init(number: Int) { self.number = number }
    
    weak var topic: Module?
    
    deinit { print("子模块 topic 数为 \(number)") }
}

var toc: Module?
var list: SubModule?
toc = Module(name: "ARC")
list = SubModule(number: 4)
toc!.sub = list
list!.topic = toc

toc = nil
list = nil

上記のプログラムを実行すると、出力結果は次のようになります:

ARC 主模块
子模块 topic 数为 4

非所有参照のインスタンス

class Student {
    let name: String
    var section: Marks?
    
    init(name: String) {
        self.name = name
    }
    
    deinit { print("\(name)") }
}
class Marks {
    let marks: Int
    unowned let stname: Student
    
    init(marks: Int, stname: Student) {
        self.marks = marks
        self.stname = stname
    }
    
    deinit { print("学生的分数为 \(marks)") }
}

var module: Student?
module = Student(name: "ARC")
module!.section = Marks(marks: 98, stname: module!)
module = nil

上記のプログラムを実行すると、出力結果は次のようになります:

ARC
学生的分数为 98

クロージャによって引き起こされる循環強参照

循環強参照は、クロージャをクラスインスタンスのプロパティに代入し、そのクロージャの本体でインスタンスを使用した場合にも発生します。このクロージャの本体は、self.somePropertyなどのインスタンスのプロパティにアクセスするか、self.someMethodなどのインスタンスのメソッドを呼び出す可能性があります。どちらの場合も、クロージャがselfを「キャプチャ」し、循環強参照が発生します。

インスタンス

次の例は、クロージャがselfを参照すると循環強参照がどのように発生するかを示しています。この例では、HTML内の単一の要素をシンプルなモデルで表すHTMLElementというクラスを定義しています:

class HTMLElement {
    
    let name: String
    let text: String?
    
    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
    
}

// 创建实例并打印信息
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())

HTMLElementクラスは、クラスインスタンスとasHTMLのデフォルト値のクロージャとの間で循環強参照を発生させます。

インスタンスのasHTMLプロパティはクロージャへの強参照を保持します。しかし、クロージャはそのボディ内でself(self.nameとself.text)を使用するため、クロージャはselfをキャプチャします。つまり、クロージャは逆にHTMLElementインスタンスへの強参照を保持することになります。これにより、2つのオブジェクトの間に循環強参照が発生します。

クロージャによって引き起こされる循環強参照の解決:クロージャを定義するときに、クロージャの一部としてキャプチャリストを同時に定義します。この方法により、クロージャとクラスインスタンス間の循環強参照を解決できます。


弱参照と非所有参照

クロージャとキャプチャされたインスタンスが常に互いに参照し合い、常に同時に破棄される場合は、クロージャ内のキャプチャを非所有参照として定義します。

逆に、キャプチャされた参照がnilになる可能性がある場合は、クロージャ内のキャプチャを弱参照として定義します。

キャプチャされた参照が決してnilにならない場合は、弱参照ではなく非所有参照を使用する必要があります。

インスタンス

前述のHTMLElementの例では、非所有参照が循環強参照を解決する正しい方法です。循環強参照を回避するには、HTMLElementクラスを次のように記述します:

class HTMLElement {
    
    let name: String
    let text: String?
    
    lazy var asHTML: () -> String = {
        [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
    
    deinit {
        print("\(name) 被析构")
    }
    
}

//创建并打印HTMLElement实例
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())

// HTMLElement实例将会被销毁,并能看到它的析构函数打印出的消息
paragraph = nil

上記のプログラムを実行すると、出力結果は次のようになります:

<p>hello, world</p>
p 被析构
その他の拡張