2016年3月23日水曜日

メモリ・ウォーズ 駆逐される巨人

最初の図。8x8マスの空間がありますが、これがRAMの2GBだとか、4GBだという容量だと考えてください。 もちろん、RAMが大きいほど広い空間になります。 灰色の空間の中にはカラーブロックが点在しています。これが実行中のアプリだと考えてください。 大きいものほど使用メモリが多いアプリです。 現在、Aのアプリを稼働中であると考えてください。 Bのアプリはバックグラウンドで待機中です。

次の図。Aアプリのプレイが続くことによって、Aアプリの使用メモリは拡大しました。 その結果、Bアプリが存在していた空間を押しつぶしました(この時点で、ホームボタンなどを押してAを中断、Bアプリを再開してみると一時保存データが消えており、 おそらくタイトルロゴか、Unityマークが登場するところから始まるでしょう)。 実際には、Aアプリは中断せずにそのまま!継続プレイ!で進めるシミュレーションとします。

次の図。Aアプリが占有する空間を"HEAP領域"と書いています。 これはどういうことか? ざっくり説明しますと Android の端末には通常ヒープが16MB、ラージヒープが32MB、みたいな設定があります。 最近の機種ほど大きくなる傾向がありますが、Xperia Z1 のコンパクト機である F-02F(Android4.4.2)、 最新のリファレンス機である NEXUS5X(Android6.0)、このあたりのマシーンは通常が192MB、ラージヒープが512MBでした。 もう少し古い端末だと、Galaxy Note N7000(Android4.1.2)で通常64MB、ラージヒープが256MB、 Galaxy Y GT-S5360(Android2.3.5)で通常64MB、ラージ設定なし、となっています。 NEXUS でいうと、LG NEXUS4(Android4.3 / 2012年末発売)から現在の192/512MBという大きめのヒープサイズになっています。 多くのメーカーがこれを基準にして、Android4.4.xくらいの端末(あるいはそれ以降の端末)は192/512MBになっているようです。 カオスなのはAndroid4.0.x~4.3.x の端末です。 しかし、デベロッパーコンソールのGoogle様の統計情報によると、ロールプレイングゲームのカテゴリでは、
////////4.1 : 12.17%
//4.03-4.04 : 9.89%
////////4.2 : 9.74%
////////4.3 : 6.35%
////////3.2 : 0.28%
2.3.3-2.3.7 : 2.62%
上記のようなシェアがあり、Android4.3以下がなんと41%も存在します。 ある時期より逆に増えたんじゃないの? 今までスマホが普及していなかった、あまり裕福ではない地域に 廉価版の安いAndroid端末が急速に普及してきたから…と考えればオールドバージョンのシェアが増えても不思議はありません。 超無責任に言うならば、15%くらいのユーザーは例え large heap にしていても、アプリのメモリサイズが 128MB を越えた時点でアウトでしょう。

ヒープとはアプリ一つに割くメモリ空間です。 Aアプリはプログラムの中で large heap を宣言しています。 端末から512MBの空間を割り当てられていると考えてください。 結構な広さの空間ですが、Aアプリは限界ぎりぎりまで育っています。 メモリ管理ができていないと、肥満化がとどまりません。 こ、こいつは…メモリリークしてんじゃないの? この先いったいどうなってしまうのか?

次の図。Aアプリの肥満化はとどまるところを知らず、 ついに"HEAP領域"を越えてしまったぞ。 いったいどうなってしまうのか?

次の図。そうです、皆さまお待ちかねの Out of Memory 発生で強制終了です。 Aが亡きあと、ずいぶんすっきりしましたね。 しかし、この国取りゲームっぽいのは面白いかもしれないですね。 パズルゲームでこういうの作ったらめちゃめちゃ流行るかもしれませんよ、Don't miss it!
生き残っているのは比較的メモリ使用量の小さいアプリばかりです。 この生き残っている部分を、OSの機関部分(システムプログラム)と考えてもよいし、 そういうシステム関係の部分はそもそも8x8マスの枠外に存在する。と考えてももらってもよいです。そんなに厳密に考えて作っていないので。

最後にまとめを。 Out of Memory 発生のメカニズムはわかってもらえたと思います。 large heap を宣言すれば、国内のユーザーなら少なくとも256MBはいけるでしょう。実際はほどんどが large heap なら512MB以上確保でしょう。 じゃあ、メモリ抑制の目安として200MBくらいで妥協しちゃっていいのか?っていう話です。 確かに170~180MBくらいでメモリ使用量が落ち着くのであれば、Out of Memory を防げるでしょう(少なくとも国内では)。 しかし、上の図を順に見てもらえば分かる通り、メモリの大きいアプリは 「他アプリからの攻撃に弱い」という弱点があります。 メモリの小さなアプリは、巨大アプリ通しの激しい陣取り合戦、仁義なき抗争の中にあっても生き残りやすい傾向があります。 生き残れば、途中からストレスフリーで再開できます(自動セーブ機能があればいい!といえばいい!のですが…)。 あと、やはり、単純に限られた資源であるメモリを無駄に使ってしまうのはユーザーに対して優しくないかな、とも思うわけです。 ラージヒープが512MBとかいうのは、初期のアンドロイド端末から考えれば10倍以上の容量に増えているわけですが、 RAMが2GBの端末でもラージヒープが512MBとかの設定をしているわけです。 RAMが2GBならシステム関係を除いた自由空間というのは1GB未満ですからね。それで、ラージヒープが512MBというのはやりすぎな感がある。

さらに海外勢の状況です。 日本の場合は機種変更を定期的にしないと「実質損をするから」みんな最新型に買い変えるわけですが、もしそういった要素がなければ? 日本でも格安simだとか、 シムロックフリーだとかの新展開が登場しました。 状況次第では、古いマシーンを大事に使い続けるっていうのはあり得ます。 日本だけでも選択肢が増えて、未来は分からなくなりました。そして、海外。 海外の情勢、といってもアメリカ市場、ヨーロッパ市場、アジア市場、いろいろあるし、インドや中東は特殊でしょう。 使用メモリが少ないことによって、意外なところからあなたのアプリに火が付くかもしれない。 だったら、もう、贅肉を落とした設計にするしかねえ。

Android のアプリでどうやったらメモリリークしにくくなるかは、色々なサイトで書かれているのでここでは簡単にしか書きませんが、一応、一筆。 まず、Activity と一部のストレージ的な役割をするクラス以外は "public class Main" と名乗らずに "class Sub" と非パブリックな形で 1行目に登録します。 非パブリックであれば、他のパッケージからは見えませんので、アクセスできません。こういう構造にすることで、アクティビティ遷移の際に参照を切りやすくなります。 さらに、メモリリークの温床となる「画像を扱う View 関係のクラス」ではフィールド、メソッドともに全て private で仕上げます。 考え方としては、必要最低限の露出にして遠くからの参照をなくすということです。 static が使いにくくなるので、苦手なインスタンスを取り扱う機会が増えましたが、仕方なし。 Bitmap はフィールドにせずにローカルで使用、使うたびに resources で拾ってきます。 Bitmap は終了時に null したらメモリが解放されるとよく書かれていますが、うまくいかなかったので…。 ただし、読み込みをやりすぎると cpu に負荷がかかるので(そりゃ同じ画像を何十回も~だから無駄の極み)、 LruCache を利用して一度読み込んだ画像はキャッシュに保存しておき、キャッシュに存在しない場合だけ resources から読み込むようにします。 LruCache のサイズはあまり大きくしなければ、例えば16MBとかなら何ら問題はないでしょう。 あとは、ググればすぐに見つかる this じゃなくて、getApplicationContext() を使え、とかそういう技を盛り込んでみます。 実際効果はありました。普通にプレイする分には50MB未満のメモリ使用でいけそうな感じです。

0 件のコメント:

コメントを投稿