iOSアプリのメモリリークの検出とデバッグの方法を、ネコに関するプログラムで学びましょう!
今日は、サンプルプログラムを使って、iOSアプリにおけるメモリリークとは何なのかを学んでいきます。「僕のイタズラ猫)」というプログラムで、メモリリークの直し方を学びます。
はじめに、Xcodeに実装されているデバッグツールを使ってメモリーの問題を探し出す方法を学びます。次に、実際にデバッグを行います。
サンプルプログラム
私はたくさんのネコを飼っています。たまにいずれかのネコがソファーを引っ掻いてしまいます。どのネコが一番「行儀が悪い」かを確認するためにこのリストを作りました。
(冗談だよ。 私は猫が大好きです。)
Githubでサンプルプログラムをダウンロードしてください:: https://github.com/mszopensource/MemoryLeakTesting
今回使うサンプルプログラムは、猫の「イタズラ度」のテーブルです。上下のボタンを押せば猫のランクを変えられます!開始をクリックすると、猫の名前の一覧のテーブルが表示されます。表示されたテーブル上では、セル1つに対してボタンが2つ付いていて、1つは猫のランクを上げるボタン、もう1つは下げるボタンです。
UITableView (catList)
-> UITableViewCells (naughtyCell)
class catList: UITableViewController {
var myCats = ["ネコノヒー", "ムギ", "レオ", "ソラ", "マル"]
override func viewDidLoad() {
super.viewDidLoad()
title = "Best Cats!"
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return myCats.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "naughtyCell") as! naughtyCell
let catName = myCats[indexPath.row]
cell.catNameLabel.text = catName
cell.catTableView = self
cell.catID = indexPath.row
return cell
}
func moveCatUp(forCatNumber: Int) {
if forCatNumber == 0 {
//最高にお利口さんのネコがこちらです。しかしそうすると、配列の範囲外参照が起きてしまうのではないでしょうか。
return
}
myCats.swapAt(forCatNumber, forCatNumber - 1)
tableView.reloadData()
}
func moveCatDown(forCatNumber: Int) {
if forCatNumber == (myCats.count - 1) {
//宇宙で一番行儀が悪いネコ。しかしそうすると、配列の範囲外参照が起きてしまうのではないでしょうか。
return
}
myCats.swapAt(forCatNumber, forCatNumber + 1)
//ネコ「forCatNumber + 1」がもっと行儀悪くなりました!!!
tableView.reloadData()
}
}
class naughtyCell: UITableViewCell {
@IBOutlet weak var catNameLabel: UILabel!
/*
もちろん、ネコの行儀の悪さは変化します。
- 2つのボタンを設けてこれに対応します。
-- 行儀悪さのランクをアップ
-- 行儀悪さのランクをダウン
*/
@IBAction func actionMoveUp(){
catTableView.moveCatUp(forCatNumber: catID)
}
@IBAction func actionMoveDown(){
catTableView.moveCatDown(forCatNumber: catID)
}
/*
そして、「catList」への参照を保存しなければなりません。なぜならネコの順番、そしてUITableViewCellsの表示順を変更したいからです。
*/
var catTableView: catList!
/*
また、どのネコのランクがアップまたはダウンしたのかがわかるように、ネコのIDを保存する変数を作ります...
*/
var catID: Int!
}
メモリ問題の発見
Xcode 内には、私たちのプログラムのデバッグとパフォーマンスの測定を行うための優れたツールが数多く存在します。
このプログラムを実行したとき、Xcode のウィンドウの左にあるこの小さなアイコンをクリックするとプログラムのマトリックスを見ることができます。
ここで「Memory」セクションをクリックして、私たちのアプリがメモリをどのくらい使用しているのかを見てみましょう。
今、下部に表示されているメモリグラフは横ばいの状態です。なぜなら、アプリ内でまだ何もアクションを実行していないからです。
ではアプリのインターフェースの「開始」ボタンをクリックしてみましょう。メモリの曲線が上向きに変化したでしょうか?
これはアプリがもう1つの「UIViewController」をスクリーン上にロードしたからであり、普通のことです。それはこれだけのスペースを使用します。
ですが覚えておいてください。もし何らかのプログラムがメモリの一部を借りてきた場合、使用を終えたらそのメモリを返すはずですよね?では「UIViewController」を閉じて、メモリ使用率が下がるかどうか確認してみましょう。
確かに下がりましたが、その下がり幅はわずかです。なぜでしょうか?
では、問題をよりわかりやすくするためにそのビューの開く、閉じるを何度も繰り返してみてましょう。ビューを10回開いてみて、どうなるか確認してみてください:
これで分かりましたね。私たちのアプリはメモリに問題を抱えています!UIViewControllerを閉じても、一部のメモリが開放されません。どうすればいいのでしょうか?
ではここで、Xcodeの2つめのツールをご紹介しましょう。メモリ確保ビューです。このツールを使うことで、メモリ内に現在何のコンポーネントがあるのかを確認できます。
Xcode ウィンドウの下部にこのボタンがあります。
詳しく見てみましょう。
このボタンをクリックするとアプリが一時停止し、Xcodeがメモリ内に何のコンポーネントが残っているかをチェックします。
ここで面白いものが見つかりました。現在メモリ内にあるコンポーネントです。
ここで何が起きたか見てください!私たちが開いた全ての「catList」がスマートフォンのメモリ内に残っています。そして、70個の「naughtyCell」があります。これらが開放されていないのは明らかです。
メモリバックトレース
このメモリツールのもう一つの優れた点は、メモリリークを引き起こしたコード内の行を表示できることです。ただし、それを行うためには、新しいメモリロギングを有効にする必要があります。
最初に、一番上にあるプログラムの名前をクリックします:
次に、「Edit Scheme スキームを編集」を選択します:
「Diagnostics 診断」タブをクリックします
クリックして「Malloc Stackスタック」を有効にします
今度は、当社のアプリを再実行して、前回取ったのと同じアクション(「catList」を10回開閉する)を実行し、メモリデバッグアイコンをクリックします。そして「naughtyCell」のいずれかをクリックします:
それから画面の右側にリークの場所が表示されるようになります:
現在、まだこの問題を解決する必要があります。
メモリリークの修正
Xcodeに含まれているツールは、メモリリークの発生箇所を検出し、表示してくれるので、非常に便利でした。しかし、この種の問題を修正するには、手作業の調査が必要です。
メモリリークのよくある原因の一つは「参照サイクル」と呼ばれれるものです。強く参照されている2つのオブジェクトが互いを参照し合うと参照サイクルが発生します。
また、どちらのオブジェクトも強く参照されているため、システムはいずれのオブジェクトもメモリから削除できません。したがって、これらのオブジェクトは永久にメモリに保存されます。
それでは、「naughtyCell」の実装を見てみましょう。
class naughtyCell: UITableViewCell {
@IBOutlet weak var catNameLabel: UILabel!
/*
もちろん、ネコの行儀の悪さは変化します。
- 2つのボタンを設けてこれに対応します。
-- 行儀悪さのランクをアップ
-- 行儀悪さのランクをダウン
*/
@IBAction func actionMoveUp(){
catTableView.moveCatUp(forCatNumber: catID)
}
@IBAction func actionMoveDown(){
catTableView.moveCatDown(forCatNumber: catID)
}
/*
そして、「catList」への参照を保存しなければなりません。なぜならネコの順番、そしてUITableViewCellsの表示順を変更したいからです。
*/
var catTableView: catList!
/*
また、どのネコのランクがアップまたはダウンしたのかがわかるように、ネコのIDを保存する変数を作ります...
*/
var catID: Int!
}
強い参照 (Strong Reference) に気付きましたか?
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "naughtyCell") as! naughtyCell
let catName = myCats[indexPath.row]
cell.catNameLabel.text = catName
cell.catTableView = self
cell.catID = indexPath.row
return cell
}
これは「naughtyCell」が「catList」を参照している例です。
var catTableView: catList!
これは「catList」が「naughtyCell」を参照している例です。
cell.catTableView = self
これらの2つのコンポーネントは互いを参照しています。:
さて、私たちは「catTableView.moveCatUp(forCatNumber: catID)」と「catTableView.moveCatDown(forCatNumber: catID)」を呼び出す必要があるため、「naughtyCell」をそれぞれの「naughtyCell」に保存する必要があることを思い出してください。解決策は?「プロトコルデリゲート」を使うことです。
デリゲート (Delegate / Protocol) は、2つのプログラミングクラス間で情報を渡すメディアのようなものです。2つの関数を持つデリゲートを作成できます:
protocol catListActionDelegate: AnyObject {
func moveCatUp(forCatNumber: Int)
func moveCatDown(forCatNumber: Int)
}
次に、型「catListActionDelegate」を持つ変数を「naughtyCell」に導入し、代わりにそのデリゲート内の関数を呼び出します。
weak var delegate: catListActionDelegate?
そして、ユーザーがアクションを実行したときに、デリゲート内の関数を呼び出します。
@IBAction func actionMoveUp(){
delegate?.moveCatUp(forCatNumber: catID)
}
@IBAction func actionMoveDown(){
delegate?.moveCatDown(forCatNumber: catID)
}
catList(UITableView)で、デリゲートを実装します:
class fixedCatList: UITableViewController, catListActionDelegate
そして、必要とされるデリゲート関数を実装する必要があります。デリゲート内の関数名が「catList」内の既存の関数名と一致するため、ここでは何もする必要はありません。
新しいメモリグラフ