概要
Xcode8から追加された「Debug Memory Graph」機能を用いてメモリリークをビジュアル化してみました。
想定したケース
- 一覧画面・詳細画面の2画面構成
- 詳細画面でAPI呼び出しを行う
- API呼び出しクラスが呼び出し元のViewControllerのあるメソッドを呼ぶ設計になっている
- 詳細画面のViewControllerクラスと、API呼び出しクラスが強参照で相互参照している
プログラム作成
メモリリークが発生するプログラムを作成します。
Storyboard
一覧画面
一覧画面は単純にTableViewで1行セルを表示し、セルをタップすると詳細画面に遷移するだけです。
遷移はStoryboard上でSegueを利用しています。
(例なので、値・Identifierは固定とし、force unwrapして表示させています。)
class ListViewController: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return tableView.dequeueReusableCell(withIdentifier: "customCell")!
}
}
詳細画面
メモリリークを発生させたいため、ViewController内であるAPIを呼ぶSomeAPICallerをインスタンス化し、ViewController、SomeAPICallerそれぞれが強参照でお互いを参照しあう状態を作成します。
import Foundation
import UIKit
class DetailViewController: UIViewController {
// API呼び出しクラスの強参照
var caller: SomeAPICaller = SomeAPICaller()
override func viewDidLoad() {
super.viewDidLoad()
// API呼び出しクラスに自身を参照させる
caller.viewController = self
caller.call()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
func updateLabelText(text: String) {
}
}
API呼び出しクラス
APIを呼び出し、成功したらViewControllerに伝える設計を想定します。
処理は特に何もさせておらず、成功した風の雰囲気で実装しています。
import Foundation
import UIKit
class SomeAPICaller {
// viewControllerの強参照
var viewController: DetailViewController?
// APIのコールを想定
func call() {
success()
}
// 成功したらViewControllerに伝える
func success() {
if let viewController = viewController {
viewController.updateLabelText(text: "new text")
}
}
}
Debug Memory Graphでメモリリークをビジュアル化
準備が整いましたので、Debug Memory Graphを用いてメモリリークをビジュアル化してみます。
Debug Memory Graphの表示
Debug Memory Graphを表示してみます。
シミュレータでアプリを起動し、XcodeのDebug AreaからDebug Memory Graphの起動ボタンを選択します。
こちらのボタンを押すと、押したタイミングでのメモリ等の状況が可視化できます。
以下のような画面です。
また、画面左下のボタンでNavigator上の表示をメモリリーク箇所のみの表示に絞ることもできます。
この時点ではメモリリークは発生していないのでなにも表示されません。
メモリリークの可視化
それでは、実際にメモリリークを発生させ、可視化してみたいと思います。
Debug Memory Graphを表示している状態だと、ブレークポイントで止まった状態と同様処理がストップしているので、Debug AreaのContinue program executionを押し再開させます。
再開したらシミューレータ上のアプリで以下操作を何度か繰り返します。
- 一覧画面の上でセルをタップし、詳細画面を表示
- ナビゲーションバー左の「一覧画面」をタップし、一覧画面を表示
上記操作後、再度Debug Memory Graphを起動し、メモリリーク箇所だけに絞ってみます。
メモリリークが発生しているため、Navigator上にメモリリーク箇所が表示されます。
DetailViewControllerと、SomeAPICallerでメモリリークが発生しているのがNavigator上からも分かります。
また、上記操作を繰り返した数によって、DetailViewController、SomeAPICallerの末尾の括弧付きの数字が大きくなっており、保持されている数がどんどん増えていることが分かります。
それでは、この2クラス間の関係性がどうなっているのか。
それを見るために、Navigator上のDetailViewControllerのメモリリーク箇所をクリックします。
画面中央のEditor上に参照の関係性を示す画面が表示され、間をつなぐ矢印をクリックすると、相互で参照し合っている関係を見ることができます。
メモリリークの除去
メモリリークを可視化することができたため、そのメモリリークの原因となっている箇所を修正します。
SomeAPICallerのviewControllerプロパティをweakキーワードで修飾します。
class SomeAPICaller {
// viewControllerの強参照
var viewController: DetailViewController?
...
}
class SomeAPICaller {
// viewControllerの弱参照
weak var viewController: DetailViewController?
...
}
上記修正を行ったのち、複数回一覧画面と詳細画面の表示を繰り返しDebug Memory Graphを起動、メモリリーク箇所だけに絞ると、Navigator上には何も表示されません。
このことによりメモリリークが解消されていることが見てとれます。
まとめ
Debug Memory Graphを用いてメモリリークを可視化することができ、相互参照の関係性も見ることができました。
Xcode8で追加されたこのDebug Memory Graphはメモリリークを解消するためのツールとして強力で有用だと思います。
また、メモリリークは発生せずともクラス間の関係性、メモリの状況を見るツールとして利用できます。
今回のケースでは単純なケースを想定しましたが、より複雑なケースで利用したときにどこまで効果を発揮できるか、検証をしていかなければと感じています。
ここに関しては、実際継続して活用し、知見を溜めていきたいと思います。