くらしのマーケット開発ブログ

猫のいる会社、みんなのマーケットの技術ブログ

Swift: Memory leaks、 豊かな人々も泣く!

こんにちは、エンジニアのDuyです。

今日はSwiftのメモリリークについて、話そうと思います。

f:id:curama-tech:20180802152853p:plain:w262:h263

最近、技術が発展しているため、モバイルデバイスの容量も大きくなっているんですが、メモリを管理しないと、アプリが遅くなって、アプリの容量が大きすぎることによって、アプリが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はnamedeviceという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

リンクしたあとは、こういう感じになります。

f:id:curama-tech:20180802160844p:plain

変数をnilにします。

staff = nil
device = nil

staff及びdevice変数によって保持されている強参照は無くなりますがが、StaffDeviceのインスタンスはまだ強参照を残ります。それはメモリリークです。

他の例を一緒に見ましょう!

(*) 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に属します。

この場合は、WorkspaceChannelのインスタンスは強参照があるので、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") 
    }
}

こういう感じになります。

f:id:curama-tech:20180802160746p:plain

この行

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/ )。