Developer's Blog

【 iOS 開発 Tips】あなたも気付いていないかもしれないメモリリークの恐怖

こんにちは、共同開発部 iOS アプリ開発担当の図子です。

iOS 開発をしている皆さんはメモリリークのチェックを行っていると思います。どのような方法で確認していますか?おそらく以下のような方法で行っていると思います。

・Static Analyzer を使う

・Instruments の Leaks テンプレート

でも、これだけでは見つからないメモリリークが起こりうることをご存じでしょうか?

addSubview: で何枚もの View を貼っていないか?

実際にメモリリークと呼べるかどうかは微妙かも知れませんが、開発者の意図しない不要なメモリがドンドン確保される可能性があるのでここではメモリリークと書かせていただきます。

非常に気付きにくいメモリリークとは view に addSubview: する際に起こりやすいのです。

以下のコードを見てください。ある UIViewController に実装されたメソッドで、あるボタンが押された時に実行されます。

- (void)addLabel {
    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(10, 100, 300, 60)];
    label.text = @"てすと";
    [self.view addSubview:label];
    [label release];
}

alloc で生成された UILabel はちゃんとメソッドの最後でリリースされています。これだけを見る分には何の問題もありません。Static Analyzer でもリークは検出されませんし、Leaks テンプレートでもリークは検出されません。
でも、このメソッドが複数回呼ばれるとしたらどうでしょう?

気付きましたか?

addSubview: はレシーバーが引数として渡された UIView を retain します。上記 – (void)addButton メソッドが複数回呼ばれた場合には UILabel は同じ位置に呼ばれた数だけ貼られていきます。貼られる View がシャドウが付いている場合にはシャドウの色が濃くなったりして見た目にも気づきやすいですが、今回の例の場合には見た目にはまったく気付けないので要注意です。

回避策

回避策は簡単です。view を addSubview: するまえに removeFromSuperview を呼んでおくだけです。
今回の例の場合はこんな対策です。

- (void)addLabel {
    /* 既に貼られている Label があれば先に剥がしておく */
    UIView *oldLabel = [self.view viewWithTag:TEST_LABEL_TAG];
    [oldLabel removeFromSuperview];

    UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(10, 100, 300, 60)];
    label.text = @"てすと";
    label.tag = TEST_LABEL_TAG; // define している tag
    [self.view addSubview:label];
    [label release];
}

この例では貼られる Label への参照を取るために tag を使いました。貼られる Label をインスタンス変数として保持している場合には tag を使わず直接剥がせば大丈夫です。addButton メソッドの初回呼び出し時には viewWithTag: の戻り値は nil になりますが、Objective-C では nil にメッセージを読んでも何も起こらないのでこの対応で特に問題はありません。

addSubview: 時のメモリリークの危険性に対しての見つけ方

私が思い浮かぶのは以下の方法のみです。

1. Instruments の Allocations テンプレートで総メモリ使用量が増加していないか確認?

2. 特定のクラスのオブジェクト(今回なら UILabel)がある動作で増え続けていないか確認?

1. の方法だと少量のメモリが増加していくだけの場合には非常に見つけにくいです。2. の方法は Instruments のサーチフィールドにクラス名を入力して絞りこみ、#Living の数が適切かどうかを確認すればいいのですが、絞り込むべきクラス名を漏れなくリストアップしチェックすることは困難です。

見つけにくい問題なので私は普段から addSubview: を呼ぶ前には removeFromSuperview を先に呼ぶ必要がないか検討しています。ミスを無くす意味で基本的に removeFromSuperview を必ず呼ぶようにしておいた方が良いかもしれません。

まとめ

メモリリークチェックツールだけでは見つけにくいメモリリークについてご紹介しました。今回ご紹介した例以外にも隠れたメモリリークがあるかも知れません。メモリリークを起こさないように細部にまで目を光らせ、メモリリークのないアプリをこれからも作っていきたいと思います。

Copyright © 2019 Fenrir Inc. All rights reserved.