57
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

iOSアプリのメモリリーク (Memory Leak) の検出とデバッグの方法を、ネコに関するプログラムで学びましょう。(Reference Cycle 参照サイクル)

Last updated at Posted at 2020-05-18

iOSアプリのメモリリークの検出とデバッグの方法を、ネコに関するプログラムで学びましょう!

今日は、サンプルプログラムを使って、iOSアプリにおけるメモリリークとは何なのかを学んでいきます。「僕のイタズラ猫)」というプログラムで、メモリリークの直し方を学びます。

はじめに、Xcodeに実装されているデバッグツールを使ってメモリーの問題を探し出す方法を学びます。次に、実際にデバッグを行います。

サンプルプログラム

私はたくさんのネコを飼っています。たまにいずれかのネコがソファーを引っ掻いてしまいます。どのネコが一番「行儀が悪い」かを確認するためにこのリストを作りました。

(冗談だよ。 私は猫が大好きです。)

Githubでサンプルプログラムをダウンロードしてください:: https://github.com/mszopensource/MemoryLeakTesting

今回使うサンプルプログラムは、猫の「イタズラ度」のテーブルです。上下のボタンを押せば猫のランクを変えられます!開始をクリックすると、猫の名前の一覧のテーブルが表示されます。表示されたテーブル上では、セル1つに対してボタンが2つ付いていて、1つは猫のランクを上げるボタン、もう1つは下げるボタンです。

Screen Shot 2020-05-18 at 15.46.03.png

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 のウィンドウの左にあるこの小さなアイコンをクリックするとプログラムのマトリックスを見ることができます。

Screen Shot 2020-05-18 at 15.46.03.png

ここで「Memory」セクションをクリックして、私たちのアプリがメモリをどのくらい使用しているのかを見てみましょう。
Screen Shot 2020-05-18 at 15.47.07.png

今、下部に表示されているメモリグラフは横ばいの状態です。なぜなら、アプリ内でまだ何もアクションを実行していないからです。

ではアプリのインターフェースの「開始」ボタンをクリックしてみましょう。メモリの曲線が上向きに変化したでしょうか?

Screen Shot 2020-05-18 at 15.49.00.png

これはアプリがもう1つの「UIViewController」をスクリーン上にロードしたからであり、普通のことです。それはこれだけのスペースを使用します。

ですが覚えておいてください。もし何らかのプログラムがメモリの一部を借りてきた場合、使用を終えたらそのメモリを返すはずですよね?では「UIViewController」を閉じて、メモリ使用率が下がるかどうか確認してみましょう。

Screen Shot 2020-05-18 at 15.51.05.png

確かに下がりましたが、その下がり幅はわずかです。なぜでしょうか?

では、問題をよりわかりやすくするためにそのビューの開く、閉じるを何度も繰り返してみてましょう。ビューを10回開いてみて、どうなるか確認してみてください:

Screen Shot 2020-05-18 at 15.52.57.png

これで分かりましたね。私たちのアプリはメモリに問題を抱えています!UIViewControllerを閉じても、一部のメモリが開放されません。どうすればいいのでしょうか?

ではここで、Xcodeの2つめのツールをご紹介しましょう。メモリ確保ビューです。このツールを使うことで、メモリ内に現在何のコンポーネントがあるのかを確認できます。

Xcode ウィンドウの下部にこのボタンがあります。

Screen Shot 2020-05-18 at 15.54.52.png

詳しく見てみましょう。

Screen Shot 2020-05-18 at 15.55.41.png

このボタンをクリックするとアプリが一時停止し、Xcodeがメモリ内に何のコンポーネントが残っているかをチェックします。

ここで面白いものが見つかりました。現在メモリ内にあるコンポーネントです。

Screen Shot 2020-05-18 at 15.57.19.png

ここで何が起きたか見てください!私たちが開いた全ての「catList」がスマートフォンのメモリ内に残っています。そして、70個の「naughtyCell」があります。これらが開放されていないのは明らかです。

メモリバックトレース

このメモリツールのもう一つの優れた点は、メモリリークを引き起こしたコード内の行を表示できることです。ただし、それを行うためには、新しいメモリロギングを有効にする必要があります。

最初に、一番上にあるプログラムの名前をクリックします:

Screen Shot 2020-05-18 at 16.02.18.png

次に、「Edit Scheme スキームを編集」を選択します:

Screen Shot 2020-05-18 at 16.03.26.png

「Diagnostics 診断」タブをクリックします

Screen Shot 2020-05-18 at 16.04.09.png

クリックして「Malloc Stackスタック」を有効にします

Screen Shot 2020-05-18 at 16.04.56.png

今度は、当社のアプリを再実行して、前回取ったのと同じアクション(「catList」を10回開閉する)を実行し、メモリデバッグアイコンをクリックします。そして「naughtyCell」のいずれかをクリックします:

Screen Shot 2020-05-18 at 16.07.19.png

それから画面の右側にリークの場所が表示されるようになります:

Screen Shot 2020-05-18 at 16.08.05.png

現在、まだこの問題を解決する必要があります。

メモリリークの修正

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つのコンポーネントは互いを参照しています。:

Screen Shot 2020-05-18 at 16.14.55.png

さて、私たちは「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」内の既存の関数名と一致するため、ここでは何もする必要はありません。

新しいメモリグラフ

スクリーンショット 2020-05-18 午後5.06.07.png

:relaxed: Twitter @MszPro

:sunny: 私の公開されているQiita記事のリストをカテゴリー別にご覧いただけます。

57
44
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57
44

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?