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未満のメモリ使用でいけそうな感じです。

2016年3月8日火曜日

アンドロイド:RAM が 2GB でも実際は…

最近、諸々の理由から スマホを2台更新しました。 今、話題の(悪い意味で)SHARP製「SH-02H」と、Google製(結局はLG製)「Nexus5X」の2台です。 今、あえてシャープにするなんて目の付けどころが悪いでしょ。 しかし、片手で持ちやすく、小型なのにフルHDです。 しかも液晶が120fps(秒速120コマ)で表示されるので、滑らかな動きがたまりません。 RAMは3GBもあります。 Xperia(Z5) のコンパクト機に性能で勝っているところがこんなにあります。 ちなみにネクサスのRAMは、2GBです。

今回はこのRAMがテーマとなります。 RAMとはマシンが扱えるデータ量ということなのでしょうか? このRAM以上の負荷をマシンにかけることはできません。 Android だと、同時に色々なアプリを開くし、インターネットやメールもすることでしょう。 一方でパソコンでも、Excel だ Word だと、いっぱい開く場合があります。 これは似ているんですけども、Android の場合は、パソコンのアプリケーションでいうところの右上「×」マークが無いんですよね。 Android4.x以降はタスクマネージメントの画面で、個別に起動中のアプリを葬ることができますが、 それでも、アプリたちの管理は原則的に"OS"に委ねられています。

Windows で、Excel だ Word だと、いっぱい開いている場合って、こまめにセーブしますよね。 だから、比較として何とも言えないんですけど、Andoroid の場合はゲームAとゲームBとゲームCとゲームDと4股していたら、 途中でネット…見た気がする。 途中で電話…かかってきたかもしれない。 途中でメール…したかもしれない。 いつの間にやらゲームAの途中データが消えていた。 こういうことがよくありますね。 これは、色々なアプリをユーザーがポコポコ開いていくうちにメモリを使いすぎて、マシンの負担が増えたことを意味します。 正確なアレはよく分かりませんが、RAMが2GBだったら、2GBに到達する前に(限界が近づいてきたら)OSが自動で実行中アプリのメモリを開放します。 メモリ解放とは、ファミコンで言うなら「リセットボタンを押す」に相当します。 この機能があるおかげで、アンドロイドさんは様々なアプリのインストールと同時並行的な使用に耐えることができるのです。

しかし、しかし。 タスクマネージメントを行う、Andoroid のOSは、もちろん常駐アプリです。 常駐アプリとは「常に駐屯している=常時起動している」アプリです。 メールとかセキュリティ系もそうでしょうね、いつ何時、情報が更新されるかわからないんだから、常時起きていて目を光らせておくしかない。 で、常駐アプリはメモリを消費します。 そして、哀しいことに、RAMが2GBあるのに実際に使えるフリー領域は半分以下の1GB弱しかない…。 ということはよくあります。 では、実際に最近買った2機種の情報を見てみましょう。 まだ、ほとんどアプリのインストールなどしていませんので、白紙状態に近いです。 アクオスフォンのSH-02Hだと、システムが1.0GB、常駐アプリが505MB、空きが1.2GBです。 最新のAndroid6.0を搭載したNexus5Xだと、空きが591MBです。 上記の値は使用状況によって変動します。しかし、まだほとんど手を付けていないので荒らされる前であることは確かです。

AndroidでOSはバージョンアップを繰り返していますが、 1.5 >>> 2.3 >>> 4.4 >>> 5.1 あたりが節目となるバージョンです。 Android2.2 の初期Galaxyを使っていたときは、2.3のゲームができずに悔しい思いをしたものです。 OSのバージョンアップは性能やセキュリティがアップするので、ユーザーにとって喜ばしいことなのですが、 しかし、新しいものほどメモリを食いますね。 Nexus5Xの、空きが600MB弱しかないのは、そういうことなのでしょう。

さきほど、RAMが2GBだったら、2GBに到達する前に(限界が近づいてきたら)OSが自動で実行中アプリのメモリを開放します。 と書きました。 これは、色々なアプリを開いて使っていくうちに、何回か前に開いたアプリの途中データが消えましたね。みたいな内容でしたが、 しかし、今使っているアプリの実行中に、まさに、2GB到達の危機が訪れたらどうなるのでしょうか? アプリは実行中に GC(ガーベッジコレクション)されるのですが、GCとはまあ不要な内部データをシュレッダーにかけるみたいな話です。 プログラムに問題があって、このGCがうまくできないとメモリがリークし続けます。 リークとはもれです。 コンクリートのひび割れに水が吸い込まれていくように、アプリからメモリがもれ続けます。 そんなこんなで、今使っているアプリの実行中に、まさに、2GB到達の危機が訪れたらどうなるか? Sowwww...「強制終了」となります。 これこそが、プログラマーが忌避する Out of Memory です。 よくユーザーレビューで「落ちる落ちるっていうけど、私は普通にプレイできましたよ。運営に文句言っているのは、最新機種に変えられない学生か何かじゃないの?」 的な煽りムンムンなお言葉を読むことができますが…。 そうなんです。Out of Memory とは環境に依存するバグなのです。 (ご使用になられているマシンの)空きメモリ次第なので、開発側も「ウソ、落ちたっていうヤツ多いな!オレのデバッグ機では普通に動いたけどな」と、首をかしげてしまいます。 勘違いしてはいけないのは、まったく同じ機種だとしても安心できないということ。A君の使い方だと落ちないし、B君の使い方だと落ちる。と、いうことがあり得ます。 この辺の事情はかなりカオスですね。混沌としております。

-------------------------------------
オレの作ったアプリ、10MBしかないから、リークしたって10MB付近でしょ?
 …そう思っていた時期がオレにもありました。 -------------------------------------

実際はアプリの容量というヤツは、そこまであてになりません。 私がメモリリークを考えずに作ったアプリなどは、10倍20倍が簡単にいっちゃいます。 では、実際に見ていきましょう。今回はタスクキラー系のアプリ「スマホ最適化」を利用させてもらっています。 使用端末は使い古したXperia Z1 SO-02F です。 とりあえず、容量の大きい四天王である4ゲームを立ち上げて、すぐに切ります。 世界線勇者110MB、ボクを殺すこと…83MB、奴は四天王…20MB、L.o.戦55MBとばらばらです。 では、トップ記録の世界線勇者を3分間プレイしてみます。 117MB、若干増えました。 ボクを殺すこと…も同様に約3分プレイ、第7ボスを倒せました。 結果として93MB、やはり若干増えました。 奴は四天王…も適当な時間をプレイ、29MB、同様の結果。ちなみにこれだけ Unity ではありません。 最後に、Legend of 戦士、これは95MBにアップ。55→95MBという大幅なメモリ増なのでメモリリークの傾向があるのかもしれません。 気になるので、もう少しプレイしてみましょう。 再開して3分くらいプレイして閉じて計測、今度は128MB、やばいですね、55→95→128MBとどんどんメモリ圧迫量が増えています。 これは連続してプレイしていると、どこかで強制終了となるかもしれません。

ちなみに、Xpreria Z1 SO-02F はもうシムカードを抜いてネット不接続状態だったのを忘れていました。 ネット接続があると、広告表示が発生するので、それ絡みでのメモリリークが発生するかも? です。 テザリングでネットを有効にして、世界線勇者を再開、約3分プレイすると…144MB、まあまあ増えましたね。 で、この時点で端末のメモリ使用量が結構増えてきました。 おろらくネット接続したことで、その他のアプリが雨後のタケノコの如く無数に発生した気配もあるんですが…(テキトー)。 ともあれ、546MB開放できます、と表示されています。 逆に言えば、それだけ使っているということです。 で、一覧の中から、ボクを殺すこと…、が消えました。 タスク管理画面ではまだ生きています。再開すると Unity のロゴが登場して最初から立ち上がる(オートセーブっぽいので前回の続きからですけど)。 これは、アプリがキラーされたことを示します。 このアプリキラーは賢いアンドロイドさんが自動的にやってくれます。 546MBともなると、だいたい80%くらいですか? メモリを使っているのでカツカツな感じです。

このXperia Z1の場合は、実際に使えるメモリが700MBくらいなんだと思います。 その中で、実用系やソーシャル系のアプリもメモリを要求しますから、実質「ゲーム枠」は300~400MBと考えていいでしょう。 もちろん、最新機種で3GB、4GBのマシンであればもっと余裕はあるのでしょうが、上は見たって仕方がない。 特に日本市場だけを見ると、世界平均よりもマシンの性能は上に位置しているハズ。 だから、2年前に入手した端末を基準にするくらいで調度いいのかもしれません。 いろいろな事情を加味すると、ハードにプレイしても100MBを越えないのが最低条件。 10分以下の標準的なプレイで、50MBを切るくらいが目標値となりましょう。

画像がどのくらい容量を使うのか? に関しては注意が必要です。 と、いうのも10KBの画像を100枚使っているから1MBくらいメモリを使ってんやろな、と思いがちですが、少し計算方法が違うようです。 例えば、同じ100x100ピクセルの絵であっても、容量が違いますよね。 私の場合、開発中の「アタックダンジョンMMF」でのモンスターグラフィックは150x100なんですが、3~8KBくらいでバラバラです。 余白が多い絵ほど、軽い傾向があります。 これが220パターンくらいあって、合計で1.2MBくらいになります。 し・か・し、実際に画像をJavaで(?)読み込む場合、同じ面積の画像は同じメモリを使います。 jpg や png などは圧縮をかけて、容量を減らしていますが、それを読み込んだ際には非圧縮です。 150x100x3(24bit=3byte)で計算すると、3~8KBの元画像が展開した結果は45KBとなります。 詳しいことはグーグルさんで各自調べてもらうとして、 想定外に大きくなる、と考えた方がよさそうです。

画像のキャッシュ機能を今作っていて、初回のみリソースから読み込んで画像データを取得して、 2回目以降はキャッシュデータを使って高速に読みこむというシステムです。 これで、キャッシュのサイズを取得すると、(まだ全ての画像をキャッシュ化できていないのに)7MB弱いっちゃってますからね。 まあ、背景とか、コマンドの元画像がデカイことが原因ですが…。 この感じでメモリーがGCされずに、もれ続けたらスゴいことになるのは容易に想像できます。 実際に私の経験上、ユーザーはもの凄く Out of Memory を連発します。 これに関しては「どうせクソみたいな低スペックマシンでやってんじゃないの?」と、なめていました。 あるいは、Galaxy というマシーンが悪いんじゃないのか? とか考えていましたが、世界的には、Samsung のシェアが Android 界隈では際立って高いですからね。 ただ単に割合の問題で Galaxy のバグが多かっただけです。 結局のところ、まともにプレイできた人ってどのくらいいるんだろう? って思ってしまいます。 この件に関しては、反省しております。直視しなければならない重大な問題だったのだよ、もっと前に。

今回は、ニューマシンも増えたので Android4.4 + 5.1 + 6.0 の3台(タブレットも含めれば4台)でデバッグができそうです。 特にネクサスはリファレンス機なので、これが基準ですよ。開発者必携のマシーンをついにゲットしたぞ。この意義は大きい。 しかし、その全てで問題なく動いても、それでも「胃の中の蛙」だと思わなければならないのでしょう。 世界中のユーザーに「安心してください、動きますよ」と保証する為には、使用メモリが少しだけ!…という裏付けが必要なのです。 この際、製作が長引いてもこの問題を解決することに注力したいものです。