本日は、前回にわずかに残した Automatic Reference Counting
の実際の挙動の最後のところから眺めていきます。そんなパートもごくわずかなので、さらっと確認してから次の 循環参照
周りの話に進んでいくか、せっかくなのでもう少し細かなアプローチで 参照カウント
周りを脱線的に遊んでみようか検討中です。どうぞよろしくお願いしますね。
————————————————————————— 熊谷さんのやさしい Swift 勉強会 #212
00:00 開始 00:48 前回のおさらい 01:31 今回の展望 02:02 インスタンス解放の流れ 04:18 初期化と代入 07:08 強参照を解消してみる 10:23 ガベージコレクションも印象深い 12:07 手動解放だったときの留意点 15:11 ARC は明快な仕組み 16:38 ガベージコレクションの特色 17:25 ARC の仕組みを把握しておくのは大切 18:58 参照カウンターは可視化できなそう 25:52 クロージングと次回の展望 —————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #212
今日はARC(Automatic Reference Counting)の実際の動きを見ていきます。今回は、参照カウントのオートマティックリファレンスカウンティング、つまりARCについての講義の最後のスライドになります。
前回からARCがどう動くかを見てきました。前回の内容では、クラスのインスタンスを保持できるオプショナル型の変数を3つ用意して、それに1つのインスタンスを初期化して代入する、という流れを見ました。つまり、3つの変数から1つのインスタンスを参照している状況を作りました。今回は、それがどのように解放されるかについてお話しします。ARCのおかげで参照カウントは隠蔽されていますが、その動作のイメージをつかんでいただくために、ゆっくりと見ていこうと思います。
まずはスライドの解説から始めます。2つの変数に同じインスタンスを代入した場合、元々の参照が3つあったうちの2つが切れて残りが1つになります。この時点では、ARCはまだそのインスタンスを解放しません。最後の1本の参照も切れると、どこからも参照されなくなった時点でARCはそのインスタンスを速やかに解放します。
具体的に見ていきましょう。前回のコードを使います。まず、以下のようなクラスを定義します:
class Person {
let name: String
init(name: String) {
self.name = name
print("\\(name) が初期化されました")
}
deinit {
print("\\(name) が解放されました")
}
}
このクラスには名前があります。初期化時にはその名前でメッセージを表示し、解放時にもメッセージを表示します。これで、インスタンスが初期化されたタイミングと解放されたタイミングがコンソールに表示されるようになります。
次に、変数を3つ用意して、1つのインスタンスをそれぞれに代入します:
var reference1: Person? = Person(name: "Alice")
var reference2: Person? = reference1
var reference3: Person? = reference1
このコードでは、まずPerson
クラスのインスタンスを初期化し、それをreference1
に代入します。そして、reference1
をreference2
とreference3
に代入します。これにより、1つのインスタンスが3つの変数から参照される状態になります。
次に、このインスタンスがどのように解放されるかを見ていきます。例えば、以下のように参照を順次解除していきます:
reference1 = nil
reference2 = nil
reference3 = nil
まずreference1
をnil
に設定すると、参照カウントが1減ります。同様に、reference2
とreference3
をnil
に設定すると、参照カウントがゼロになります。この時点でARCはインスタンスを解放します。この動作の過程を、スライドやコードを使ってさらに詳しく説明していきます。
ここまでが今回の内容です。ARCの働きを理解することで、Swiftのメモリ管理がどのように行われているのかを深く知ることができます。次回以降も引き続き、Swiftの言語仕様やメモリ管理について学んでいきましょう。 そのため、20行目までに3か所で参照が共有されています。これによってARC(Automatic Reference Counting)で管理されていることがわかるでしょう。この共有されている参照が存在する限り、メモリ空間にあるインスタンスは解放されません。これは、そのような仕組みを説明するための話です。
ここで、もう一度スライドを使って説明します。この2行の代入によって参照を解除していくというお話です。実際にこれをやってみましょう。まず、このまま実行してみます。そうすると、「初期化された」というメッセージが表示され、それ以外には何も出ていない状況になります。これは、「初期化」が実行されたことを示しています。
具体的には、インスタンスが1回だけ確保されて、それを3回代入しているという感じです。この中で代入の順番は関係なく、例えば最初に代入した参照 reference1
に対して nil
を代入すると、この時点で reference1
は nil
を指すことになります。つまり、reference1
が何も指さない状態です。これで参照カウントが1つ減るわけです。しかし、まだ reference3
が参照しているため、インスタンスは解放されません。
次に、reference2
を解放します。ここで reference1
が何も指さない状況になりました。さらに reference2
を解放すると、2か所の参照が消えて、reference3
だけが参照を持っている状態になります。しかし、まだ1つの参照が残っているため、解放処理は始まりません。
最後に、reference3
に対して nil
を代入すると、インスタンスは解放されます。こうして「解放された」というメッセージが表示されます。これを裏で処理しているのがARC(Automatic Reference Counting)です。これは画期的なもので、以前からあるスマートポインターの考え方に基づいています。ARCが登場してからは、これが当たり前のようになっています。
もともと、参照がされなくなった段階で自動解放する仕組みとしては、Javaのガベージコレクションが有名です。Javaのガベージコレクションもすごい発想ですが、ARCの方が自然に感じられるかもしれません。ガベージコレクションは参照カウンターを持たないのか、持つのか、その管理方法に詳しくないので断言できませんが、その辺もよく考えられています。
Javaはかなり昔から自動解放の仕組みに力を入れてきましたが、それに比べてARCはシンプルで高速です。とても優れた仕組みだと思います。自動解放がなければ困るのは、メモリが解放されないという事態でしょう。 ARCがないと困るのは、メモリ管理が複雑になることです。昔は当たり前のように手動でメモリを管理していました。例えば、ARCがなかった場合、あるオブジェクトを100回でも1000回でも1万回でも繰り返し使用するコードを書くとき、初期化したインスタンスを使い終わった後に手動で解放しなければならなかった時代があります。具体的には、インスタンス = パーソン
のように名前を設定して使用し、C++のようにイニシャライズしたものを使った後に手動で解放する必要がありました。
昔の言語ではこのような手順が常識でした。しかし、コードが複雑になるにつれて、条件式やガード文を多用するようになると問題が発生します。例えば、ガード let インスタンス = 名前 else { continue }
のようなコードを書くときです。このような条件付きのコード内では、メモリが解放されるタイミングを見失いがちで、100回以上のガードが関わるとメモリリークが発生する可能性があります。これがARCのおかげで、ガードから抜けるタイミングで自動的にメモリが解放され、管理が非常に楽になります。
Swiftではガベージコレクションを採用せずにARCを採用しています。これはメモリの自動管理をシンプルにする一方で、プログラマーがARCの概念をよく理解している必要があります。ARCは、スコープを超えた時点で自動的にオブジェクトを解放し、デストラクタが適切なタイミングで呼ばれる仕組みです。これにより、メモリ管理が非常に容易になります。
一方、ガベージコレクションでは、あるタイミングでシステムが勝手にメモリを解放するため、解放のタイミングが把握しづらいという問題もあります。これは非常に怖いことです。例えば、循環参照によるメモリリークのリスクがあります。しかし、ガベージコレクションでは大元のオブジェクトが参照されなくなったときにメモリが解放されるなどの仕組みがあります。
Swiftにおけるメモリ管理で重要なのは、このリファレンスカウンタの概念を理解しておくことです。この理解があると、コードの品質が向上します。参照には強参照、弱参照、無償参照の3つのスタイルがあり、これらを使い分けることでARCを制御できます。
SwiftではARCを無効にすることはできないため、これらの参照スタイルを使ってメモリ管理を行います。例えば、リファレンスカウンタの確認をSwiftで行う場合、一部の方法はObjective-Cのランタイム機能に依存しています。リテインカウント
というプロパティを使ってカウントを確認する方法があるものの、Swiftのネイティブコードでは見つけづらいことがあります。
リファレンスカウントのプロパティを調べる際には、クラス CFType
をキャストし、CFGetRetainCount
を使用する方法もあります。これにより、ARCの動作やリファレンスカウントを詳しく確認できます。 リテインカウントについて話しましょう。リテインカウントというのは、特にCF系(Core Foundation系)でいろいろと重要な概念です。ただ、このままではうまくいかないですね、リテインカウントはメソッドとして認知されています。インスタンスメソッドの一つで、「リテインカウント」は確実に稼働しています。
例えば、NSUインテリアテリティカウントというメソッドプロパティがありますが、それを使うかどうかで迷うこともあるでしょう。また、プロトコルも絡んでくるので、オブジェクティブCではややこしく感じるかもしれません。でも心配しないでください。
コードを修正するために、リテインカウント
をUイント
型にキャストしてエラーを解消します。以下のように、インポートファンデーションを使ってエラーなく進められます。
import Foundation
35行目のエラーもなくなります。プレイグラウンド環境ではうまく動作するかどうか確認が難しいですが、リファレンスカウントは取れました。プロトコルやダミープロトコルも絡んでくるので、オブジェクティブCのインターフェースに特化した部分です。
次に、ターミナルアプリで実行してみます。メイン関数にコピーしてビルドすると、やはり正常に通ります。同様に処理を行って数字を出力しますが、プレイグラウンドでは信憑性の高い数字は取れないようです。でも参照カウントがゼロになったらオブジェクトが解放されることはわかりました。
もう一度試してみましょう。anyobject
でキャストすることで、違う数字が出ることがあります。インスタンスの引っ越しに関する事情もありそうです。整理すれば、もう少し正確なリファレンスカウントが得られそうです。
ARC(Automatic Reference Counting)がゼロになるタイミングで自動解放される話を理解していただければ大丈夫でしょう。次回はこれらをさらに踏み込んで、共参照循環やクラスインスタンスの問題について話していこうと思います。
今日の勉強会はこれで終了です。お疲れさまでした、ありがとうございました。