なぜAndroidではゲームアプリがヌルヌル動かないのか

12/3に引き続き、12/9も担当することになりました@diostrayです。
justInCaseで多分Androidアプリの開発担当もすることになりそうなので、そこからちょっと派生した話題を書きます。

【注意】
この記事はネタに走ります。不快に思われた方はすぐにブラウザバックよろしくです。

# TL;DR
- AndroidのVM/Heapサイズについて
- JNIについて

をゲームアプリの負荷という観点から説明します。

# はじめに
皆さん、スマホゲームやってますか? 私は2011年くらいからほぼ何らかのゲームをやってる気がします。思い出す限りこんな感じだったはずです。

- パズル&ドラゴンズ
- チェインクロニクル
- テラバトル
- アイドルマスターシンデレラガールズ(一瞬だけでしたが)
- シノアリス
- スターオーシャン アナムネシス
- Fate/Grand Order(現役)[^1]

これらのゲームでよく言われるのは、**Androidでやるとか情弱、iPhone一択やろ**。
まぁ実際問題その通りなんですが、「Androidの方が(機種にもよりますが)スペックは良いはずなのに、特にゲームとなるとiPhoneの方がヌルヌル動く[^2]」という経験のある方は多いと思います。

私も出た当時のFate/Grand OrderをAndroid(確かGalaxy NEXUS)でやった時はカックカクで発狂しそうになった経験を思い出します。

ではどうしてなんだろう?という割と純粋な疑問に答えてみよう、というのがこの記事の趣旨です。
主にAndroid側を解説するので、iOS側を期待された方には申し訳ないですm(_ _)m

# おことわり
- ゲームアプリ自体を作ったことはないので、ソースの構成はちょっとわからないです

# そもそもAndroidのHeapサイズは?
この問題を語る上で避けて通れないのが、Androidは`Java`だということです。
Javaで欠かせないもの。それは皆さんご存知の`Heapサイズ`ですね。
Heapサイズと言ってもPermanent領域やらEden領域やらいろいろあります[^3]が、まず重要なのは最大Heapサイズです。
※Androidは厳密にはJavaVMではなくて、ちょっと前まではDalvikVM→今はAndroid Runtimeですが、本筋ではないので脇におきます

さて、ここにとあるAndroid端末(SonyMobile製 Xperia XZ)があります。

[公式サイトのスペック表](https://www.sonymobile.co.jp/xperia/docomo/so-01j/spec.html)によると、RAM 3GB。
(最新端末には一歩譲りますが、まぁこんなものでしょう)

さてここでクエスチョン。この端末のHeapサイズはいくつでしょうか?

```
1. 1GB
2. 512MB
3. 256MB
4. それ以下
```

正解は…【4の192MB】でしたー!
<img src="https://qiita-image-store.s3.amazonaws.com/0/3423/91793634-9961-90e3-8311-00835c5e6dd0.png" width="40%" />

…え?そんなに少ないの:interrobang:と思われた方。そうです。~~私が変なおじさんです~~そんなに少ないんです。[^4]

ここで「端末のメモリサイズとHeapサイズがなんでこんなに違うのん?」疑問に思う方も多いと思いますので、解消していきたいと思います。
（ここからは結構マニアックな話になります）

## AndroidアプリのVMについて
さて、AndroidのHeapサイズが予想以上に小さいのはなぜかということを理解するには、Android上でVMを起動する単位が密接に関係しています。

特にWebシステムとかですと、Tomcatで一つのJavaVMが起動して、その中にWebアプリがいくつも起動していることが多いので、Androidも同じじゃないの?と思う方が多いと思います。
<img src="https://qiita-image-store.s3.amazonaws.com/0/3423/8d803362-766a-e746-a220-8395b8cca846.png" width=80% />


ところがAndroidは（最近流行りのDockerのように）、それぞれのアプリがVMを持っているイメージになります。
<img src="https://qiita-image-store.s3.amazonaws.com/0/3423/46266407-8509-8bcf-2ce7-68a39f9bf2cc.png" width=80% />

なので、個々のアプリのVMのHeapサイズは小さくせざるを得ないという制約に繋がります。[^5]

アプリごとにVMが起動し、かつそのHeapサイズはあまり多くないので、時と場合によっては画像とかのメモリを開放しないと悪名高き`OutOfMemoryError`に容赦なく襲われます。[^6]
そのため、一旦メモリ上にロードした画像ファイルを開放して空きメモリを確保→空いたらまたロードする的な処理を作り込まないといけない場合があり、結果として高画質の動画を再生する手前とかはカクカクしてしまうことになってしまうわけです。

一方iPhoneはほぼ端末に搭載しているメモリ(最近の端末だと2GB〜)を一つのアプリが独占することも可能（当然OS分を除いた分になりますが）なので、Androidからすると富豪的にメモリを使えるわけですね。
まずここが大きな違いとなります。


### ここまでのまとめ
- AndroidはアプリごとにVMが起動する
- そのHeapサイズがあんまり多くないので、メモリ管理には気を使ったプログラムにしないとダメな時がある
- その分動作は犠牲になったのだ…

## 余談（Zygoteについて）
さてここで、「嘘だッ！(AndroidのアプリごとにVMが起動するのはわかったけど、アプリの起動自体は早いんだから)」という疑問を持った方もいるかと思いますので、少々補足します。
この疑問を持つ方はjarのロードの仕組みを知る必要があるので、Javaに詳しい方だと思います。

実は、AndroidOSの起動時に、全てのアプリの元になる`Zygote`というプロセスを起動します。[^7]
`元になる`というのは、アプリを起動する時に次のようなフローをたどるからです。

1. アプリの起動イベントがコールされる（アイコンクリックとか）
2. AndroidOSは、 Zygoteプロセスをforkして、アプリ用のプロセス（VMもコミで）を生成する[^8]
3. 新しく作ったプロセスに、アプリのManifestとかを当てはめ、アプリとして起動する

つまり素のJavaVMとして必要な起動は出来ていて、アプリとしての起動のフットプリントを極限まで小さくしているので、VM別の割に起動が早い、ということを実現できています。
いやこの仕組み考えて実装した人すげーわ…。

ここはちょっと余談ですね。

# JNIについて
そしてAndroidが`Java`であることで、もう一つパフォーマンスに大きな影響を与えるもの。
それが`JNI`です。

JNIについて触れるとめちゃくちゃ長くなるので省略しますが、要するにJavaVM内でC/C++のソースを動かすための仕組みです。
JavaのメソッドからC/C++のメソッドを呼び出して、Javaのメソッド内で戻り値を受け取るという~~変態な~~ことができるものです。

んで、このJNI。仕組みとしては素晴らしいものなんですが、あんまりパフォーマンスが良くないです。[^9]

そのため、C/C++の資産を使う傾向にある（2D/3Dレンダリングとか、動画再生とか）ゲームアプリとは相性があんまり良くないということに繋がります。

# まとめ
- AndroidアプリのVMはiOSとは比べ物にならないくらい小さい
- 特にC/C++を使っているとパフォーマンスが悪くなる傾向がある

以上の2つの観点からAndroidとゲームアプリの相性について説明してみましたが、いかがでしたでしょうか?
とは言ってもAndroid自体がダメとは**一切思っていません[^10]**ので、不快に思われた方はそのあたりご理解頂けると恐縮です。

## 参考
* http://dsas.blog.klab.org/archives/52003951.html
* http://www.atmarkit.co.jp/ait/articles/1611/25/news019.html


[^1]: 最近星5に嫌われてます(血涙)
[^2]: 体感には個人差があります
[^3]: Java10以降は名前が変わったはず
[^4]: 192MBも昔からするとめっちゃ増えてます。Android2.3とかの時代だと2ケタMBはザラでした…
[^5]: アプリごとのVMHeapを大きくすると、同時に起動できるアプリ数が小さくなる
[^6]: 1MBのJPEG画像…BitmapFactory…OutOfMemoryError…うっ頭が
[^7]: Stacktraceの一番下に出てくる`Zygote.init(...)`ってあるヤツですね
[^8]: この辺を担当しているのが`ActivityManager`です
[^9]: C/C++が遅いという意味ではないです。JNI自体、要するにJavaとC/C++の境界線のパフォーマンスが悪いのです。
[^10]: iOSよりもAndroidの方が優れているところもあると思っています。特に`Intent`や`ContentProvider`。