2012年03月10日

android のメモリ管理と出会う旅 [後編]

 昨日から NDK (Androidアプリを C/C++ を使って開発する環境) を触り始めましたが、うわーあー超楽しいな!。Javaも新しい事を知るって意味では楽しかったけど、基本不自由を強いられるだけの緊縛プレイなので、久々に何の縛りもないのが楽しすぎるったらもう!
 もうテクスチャまでは出てるので、あとはひたすら Java から C++ に書き換えるだけ。

 で、昨日 の続きです。android のメモリのモニターはできるようになったので、いろいろ触りながらメモリ状態を見比べてみたところ、いろんな事が分かりました。
 Java触りはじめて1ヶ月そこら、しかも本を読まない人の言う事ですから、世間一般とはちょっと考え方が違うかもしれませんが、自分で試した結果そうなったんだから、僕はそれを信じます。そんなお話。

脱!static 宣言!

 元来、Java だろうが C++ だろうが、static 変数がやたら多くなる人は設計に問題がある事の方が多いのですが、android の場合は OS の都合もあって、static はなおさらタチが悪いです。

 android を操作していて、BACK でホーム画面まで戻った場合、一見アプリは終了したように見えますが、実際には Activity が終了しただけで、アプリのプロセスと static 変数はメモリに残っています
 この場合、static 変数が多ければ多いほど無駄なメモリが残る事になりますが、いくら残ってたところで他のアプリでメモリが足らなくなったら自動的に始末されるので、ユーザーにとって迷惑と感じる事はないはずです。

 が、これの真の問題は、一見アプリが終了したように見えるのに、static 変数だけは消えずに残っていて、次回起動時もそのまま使われる事です。
 この状態で同じアプリを起動すると初期画面から始まるので、一見まっさらで起動したように見えるのですが、static 変数だけは前回起動時の値です。以下のようなケースも初回起動時しか初期化されないので注意です。
// 宣言と同時にnewしたstaticオブジェクトの中身
static MyClass mMyObj = new MyClass();

// staticブロックを使っての変数の初期化
static {
    mCount = 0;
    mMyObj = new MyClass();
}
 対処としては、以下のような感じでしょうか。でもこれを心がければ、アプリ終了後に残るメモリも最低限になるので精神的に清々しいです。
  • static 変数の宣言と同時に new したり、static ブロックで初期化するのは避けて、起動時に初期化したい static 変数はアクティビティの onCreate などで初期化する
  • そもそも static 変数を可能な限り使わない。アプリ各部で使いたい共有パラメータなど、どうしても必要なら シングルトンパターン にすれば再初期化しやすい。
  • アプリ終了後に残るメモリを最小限にしたければ、static のオブジェクトは不要になったらできるだけ null に戻す。(実益はないけど)

俺がお前で お前が俺で

 Java におけるメモリリークについてググっていたところ、こちらの記事を見つけました。  なるほど。いろんなところに罠があるもんですね。つーかシステムメソッドがこっそり参照しといてリークって酷くね?。とか興味深く読ませていただきましたが、問題1がどうにも合点がいかない。

 アクティビティの onCreate の中で、アクティビティ自身を渡した TextView を作り、それを自分のコンテンツとして登録する。アクティビティを作る上で、あまりに当たり前のコードです。
 アクティビティが終了すれば、つられて TextView も始末されるはずだし、問題ないような、でも TextView がアクティビティを参照してたらそもそもアクティビティが始末の対象にならないような。あれえ?

 と、どうググればいいものか分からず苦労しましたが、こういうのは 循環参照というらしく、Java のガーベジコレクタなら普通はちゃんと始末対象になる みたいです。実際、同じコードのアクティビティを作り、アクティビティの終了後に始末される事を確認しました。  例えば以下のようなコードだとしても、ちゃんとアクティビティ終了後に MyActivity も mMyClass も始末されるようです。
// MyActivity を参照するクラス
public class MyClass
{
    private Context    mContext;

    public MyClass(
        Context context)
    {
        mContext = context;
    }
}

// MyClass を使用するアクティビティ
public class MyActivity extends Activity
{
    private MyClass    mMyClass;

    @Override public void onCreate(
        Bundle savedInstanceState)
    {
        super.onCreate( savedInstanceState );
        setContentView( R.layout.main );

        mMyClass = new MyClass( this );
    }
}

たまにしぶとい人が居る

 今週の週報 で紹介した僕の初 Java アプリは、とにかくまあ動けばいいやということで、static でいろんな物を保持しまくってたので、そらもう最初にメモリダンプした時はヒドイもんでしたが、おかげさまで今では意図しないオブジェクトはほぼ残らなくなりました。

 が、最後までどうしても意図通りに始末されないオブジェクトがありました。GLSurfaceView から派生したクラスです。GLSurfaceView は OpenGL で描画するのに誰もがお世話になる便利なビューですね。
 こいつだけは、アクティビティが終了した後もなぜか始末されません。おまけに、アプリを再起動するたび増えます。完全にリークじゃん。なんだこれ。

 ちなみに、ガーベジコレクトされないというのは、誰かがそのオブジェクトを意図してない所から参照してるからなので、誰が参照しているかが分かれば原因も分かるはずです。我らが Memory Analyzer 様はもちろんそれもお見通しですよ!
 Memory Analyzer でオブジェクトの一覧が出ている状態で、オブジェクトを右クリックして [Path To GC Roots] > [with all references] を選ぶと、そのオブジェクトを現在参照している箇所の一覧が表示されます。
20120310a.png
 ここに見覚えのあるクラス名や変数名があればしめた物ですが、どうにも大抵は見覚えのないシステムクラスか循環参照の類ばかりで、こちらで解除できる参照ではない事のほうが多いです。GLSurfaceView の場合もそうでした。

 さてこれはいったい何だろう、といろいろググっても分からず、確認のためもう一度ダンプしてみたんですが、いつのまにか始末されてやんの。その間4〜5分。増殖してた分も全部なくなってました。何度やっても4〜5分かかるものの、確実に始末はされます。
 正確な理由は分かりません。スレッドを起動するクラスだとそうなるんじゃないかと勝手に推測してますが、何らかの都合で、アプリ的に不要になってもしばらくの間はガーベジコレクトされないタイプのオブジェクトがあるようです。
 オブジェクト自体が大して大きくないなら問題ないでしょうが、たまたまそのクラスが中で大量にメモリを確保してたりするとマズいかもしれませんね。


 そういえば、みんなのデバッグのお供 Toast さんの Context に Activity の this を渡した場合も、その後なかなか Activity が始末されなくなる ようです。
 ググったら まんまジャストのツイート が出てきたもので、なんだよバグかよとか一旦思考をやめてしまったのですが、後で調べたら、やっぱりこれもしばらく待てば始末されてました。

 ただ、Toast さんについては、出した Toast よりも先に Activity が死んでしまうケースもあるので、Context に Activity の this を渡すのはあんまよくないようです。以下のようにして最初から Activity を参照させない方が後腐れありません。
// 不安な例:Context に Activity 自身を渡す
Toast.makeText( this, "Succeed", Toast.LENGTH_LONG).show();

// 安心な例:Activity ではなく、Application の Context を渡す。
Toast.makeText( getApplicationContext(), "Succeed", Toast.LENGTH_LONG).show();

ネイティブメモリは Memory Analyzer の対象外

 こんだけOS全部のメモリを把握しているかのような Memory Analyzer さんですが、よくよく調べていると、OpenGL で使用しているテクスチャや頂点バッファのメモリがカウントされていない事に気付きます。
 デバッガでダンプしたり、Memory Analyzer さんで解析ができるのは、java のプログラムが動いている DalvikVM が管理している分だけなんですね。
 OpenGL はデバイスの物理メモリに直接アクセスしたいので、そのようなメモリは Java の Buffer オブジェクトの allocateDirect で確保しないといけませんが、その場合はメモリの管理の対象外になるようです。

 とはいえ、Buffer オブジェクトが始末されれば確保したメモリも始末されるはずなので、自分のクラスが確保した Buffer オブジェクトが一つも残ってないのであれば、少なくとも無駄なメモリは残っていないはずです。

 GL 関数で作ったテクスチャ、バッファ、シェーダプログラム等については、これまたCの国の人としてはタイヘン気持ち悪いのですが、GLSurfaceView の onPause() が呼ばれた時点で勝手にまとめて始末される ようで、その後アプリが復帰して onSurfaceCreated() が呼ばれた時に自力で元の状態に戻さないといけません。めんどいなぁ。
 何が驚いたかって、いつどうやって始末されてるのかよく分からんのでググってみたら、そんなこと気にしてる人がほぼ皆無だった事に驚いた。

posted by ひこざ at 23:14| Comment(2) | 開発 - Android
この記事へのコメント
ひこざ様
御多忙の所、場違いなコメントで申し訳御座いません。

記事を拝読致しました。アンドロイド開発における有効なメモリの使い方について、個人的にご意見を頂くことは可能でしょうか。

敬具
Posted by 柳田 at 2013年08月29日 16:10
コメントありがとうございます。

実のところ、この記事を書いてから1年以上アンドロイドに触っていないもので、有用な回答ができるかどうかは分かりませんが、それでもよろしければ右上の「Hikware」のバナーの先にメールアドレスが記載されておりますので、そちらまでお送りください。

OSについても、この記事は android 2.x での話ですので、今は全然違うのかもしれません。
Posted by ひこざ at 2013年08月30日 11:19
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。