こんにちは、エンジニアのDuyです。
今日はSwiftのメモリリークについて、話そうと思います。
最近、技術が発展しているため、モバイルデバイスの容量も大きくなっているんですが、メモリを管理しないと、アプリが遅くなって、アプリの容量が大きすぎることによって、アプリがcrashする可能性もあります。お金持ちなのにお金を無駄遣いしたら、ある時点でお金がなくなってしまう。お金がなくなったら、豊かな人々も泣く!
そのため、メモリを管理するのが大切です。
つまり、メモリリークとは?
メモリリークはプログラムが確保したメモリの一部、または全部を解放するのを忘れ、確保したままになってしまうことを言う。そのメモリは参照されないので、開放できないし、利用もできない。
リークはいつ?どこから?
アプリを開発するとき、サードパーティのライブラリを使ったら、リークが発生する可能性があるし、CALayerやUILabelのようにAppleによって作成されたクラスからリークが発生する可能性もあります。さらに、デベロッパーのコードはリークがよく発生します。(残念なんですけど、それは本当のことです!)
ある朝、目を覚まして、綺麗な空を見ます。あなたは出社して、自分の席に座って、前日の困っていたことを突然スムーズに解決できますが、アプリを何回かテストしているうちに、だんだんと遅くなってしまいます。あなたがリークを作成しました。(実は寂しくてもうれしくても、リークを作成する可能性があります。)
どうやって、リークを避けることができるでしょうか? リークが発生したら、どうやって、リークを解決できるでしょう?
リークを避ける、あるいはリークを解決できるようにするため、Swiftのリークの原因をさがしましょう!
強参照サイクル (Strong Reference Cycles)
強参照は、参照が存在する限り参照が指すクラスインスタンスを割り当て解除できないことを意味します。下記の例を一緒に見ましょう!
(*) StaffとDeviceの例:
class Staff { let name: String init(name: String) { self.name = name } var device: Device? deinit { print("Staff \(name) is being deinitialized") } } class Device { let name: String init(name: String) { self.name = name } var staff: Staff? deinit { print("Device \(name) is being deinitialized") } }
Staffはname
とdevice
という2つプロパティがあります。Staffは必ずdevice
があるとは限らないので、device
はオプショナルです。
類似、Deviceは2つプロパティがあります。nameとstaffです。
今、変数を定義します。下記の変数のイニシャル価はnil
です。
var staff: Staff? var device: Device?
次に、StaffとDeviceのインスタンスを上の変数にアサインします。
staff = Staff(name: "yamada hanako") device = Device(name: "macbook")
2つのインスタンスをリンクします。
staff!.device = device device!.staff = staff
リンクしたあとは、こういう感じになります。
変数をnilにします。
staff = nil device = nil
staff
及びdevice
変数によって保持されている強参照は無くなりますがが、Staff
とDevice
のインスタンスはまだ強参照を残ります。それはメモリリークです。
他の例を一緒に見ましょう!
(*) Playerの例:
class Player { let name: String let age: Int lazy var detail: () -> String = { return "Name: \(self.name), age: \(self.age) " } init(name: String, age: Int) { self.name = name self.age = age } deinit { print("Deallocated!") } } var player: Player? player = Player(name: "yamada", age: 20) player?.detail() player = nil
これから見ると、player
がnilに設定されている時にPlayerインスタンスが割り当て解除されないため、deinitのprintを実行されません。それもメモリリークです。
先程、リークを作りましたが、どうやって解決しますか?
弱参照(Weak Reference)
弱参照は、それが参照するインスタンスを強く保持しない参照です
弱参照は参照するインスタンスを強く保持しない参照であるため、ARCが参照インスタンスを破棄しないようにします。(オフィシャルドキュメント)
(*) StaffとDeviceの例をもう一回見ましょう!
もし、var staff: Staff?
の行は weak var staff: Staff?
にしたら、下記のスクリプトを実行して、
staff = nil device = nil
結果は
Staff yamada hanako is being deinitialized Device macbook is being deinitialized
というのは、StaffとDeviceのインスタンスを割り当て解除されました。リークを解決できました。
weak
にはよくThe Delegation Patternを使います。例えば:
open class UITableView : UIScrollView, NSCoding, UIDataSourceTranslating { public init(frame: CGRect, style: UITableViewStyle) // must specify style at creation. -initWithFrame: calls this with UITableViewStylePlain public init?(coder aDecoder: NSCoder) open var style: UITableViewStyle { get } weak open var dataSource: UITableViewDataSource? weak open var delegate: UITableViewDelegate? [...]
または、
@IBOutlet private weak var label: UILabel?
これを見ると、weakはnilをセットできるので、定数にできません。
非所有参照 (Unowned Reference)
弱参照と同様に、非所有参照は、それが参照するインスタンスを強く保持しません。 ただし、弱参照とは異なり、他のインスタンスが同じ存続時間またはより長い存続時間を持つ場合、非所有参照が使用されます。(オフィシャルドキュメント)
例を見ましょう!
class Workspace { let name: String var channel: Channel? init(name: String) { self.name = name } deinit { print("Workspace \(name) is being deinitialized") } } class Channel { let name: String let workspace: Workspace init(name: String, workspace: Workspace) { self.name = name self.workspace = workspace } deinit { print("Channel #\(number) is being deinitialized") } } var workspace: Workspace? workspace = Workspace(name: "slack") workspace!.channel = Channel(name: "private", workspace: workspace!) workspace = nil
Workspaceはchannelがあるかどうか決まっていないがChannelは必ずWorkspaceに属します。
この場合は、Workspace
とChannel
のインスタンスは強参照があるので、deinitの関数を実行されません。
しかし、unowned
をつかったら?
class Channel { let name: String unowned let workspace: Workspace // unowned を追加 init(name: String, workspace: Workspace) { self.name = name self.workspace = workspace } deinit { print("Channel #\(number) is being deinitialized") } }
こういう感じになります。
この行
workspace = nil
を実行すれば
Workspace slack is being deinitialized Channel #private is being deinitialized
理由は、非所有参照のため、workspace変数が保持する強参照を解除すると、Workspaceインスタンスへの強参照はなくなります。メモリリークを解決できました。
キャプチャーリストを定義する(Capture List)
キャプチャリストは、weakまたはunownedキーワードをクラスインスタンス(selfなど)への参照や、値によって初期化された変数(delegate = self.delegate!など)とペアにしたものです。(オフィシャルドキュメント)
Playerの例をもう一回見ましょう!
lazy var detail: () -> String = { return "Name: \(self.name), age: \(self.age) " }
変更して、
lazy var detail: () -> String = { [unowned self] in // [unowned self] を追加 return "Name: \(self.name), age: \(self.age) " }
もう一回実行したら、結果は
Deallocated!
というのはリークも解決できました。
最後に
私たちテックチームでは「くらしのマーケット」を一緒に作る仲間を募集しています。興味がある方は是非お気軽にご連絡ください (コーポレートサイト https://www.minma.jp/ )。