Android で Resources から getDrawable() などして画像をとった場合、一体それはいつどんなタイミングでメモリに読み込まれているのか? キャッシュはされているのか?
いい加減把握しておきたかったので、これも調べました。
API 17(4.2.2_r1)で見た結果なので、他のバージョンのSDKでも同様かはわかりませんが、多分同じ感じだと思います。
リソース画像は getDrawable() 時に初めてメモリにロードされる
リソースの画像ファイルは、いい感じに起動時にロードしてメモリにキャッシュしてくれているのだろうと思っていたのですが、 普通に「Resources#getDrawable() 時に初めてファイルからメモリへロードされる」というのが正解でした。
そもそも、Resources からメモリにビットマップが展開されると、デフォルトの RGBA_8888 で読み込まれるため、 たとえば読み込まれた結果 600 x 800 ピクセルになる画像でも、色情報だけで600*800*4byteで約1.9MBとなり、 全部ロードしておくなどというのは到底無茶な話でした。
一度読み込んだビットマップは Resources がキャッシュしてくれています。 ただし WeakReference で保持されているため、実参照を切ったら割と簡単に消えそうです。
まとめると以下のようになります。
- getDrawable() のタイミングでファイルI/Oが発生する
- Resources の画像はキャッシュはされるが揮発性が高い
ちなみに上記は BitmapDrawable についての話でしたが、 ColorDrawable についても同様で、 getDrawable() のタイミングで生成されており、同様に WeakReference によるキャッシュも作成されていました。
一部のシステムリソースは常にメモリに乗っている
getDrawable() 時にファイルからロードされると書きましたが、一部のシステムリソースは preload としてシステムの起動時に読み込まれ、常にメモリ上に乗っているようです。
なので、このシステムリソースを使う際は、ファイルI/Oなし、かつ追加でメモリを消費することなく使えてお得そうです。
具体的には以下に書かれた Drawable が preload としてシステムの起動時に読み込まれていました。
- core/res/res/values/arrays.xml
- preloaded_drawables
- preloaded_color_state_lists
(おまけ)画面密度ごとに画像ファイルを作るべき?
リソースを読み込む際、ざっと見た感じ density の値を考慮して、BitmapFactory.Options の inScreenDensity を設定して Bitmap を読み込んでいます。
そのため、メモリ上に保持される Bitmap は画面密度にあったサイズ(容量)になります。
「画面密度の小さい端末(例えばmdpi)で、大きい画面密度の画像(例えばxhdpi)を読み込むとメモリを余計に食うので、画面密度にあった画像を用意するべき」 という tips がありますが、これって正しいの?という疑問を持ったので、こちらも軽く調べました。
結論から言うと、「自分より大きい画面密度の画像に限定されず、画面密度が異なる画像を読み込むと余計にメモリもCPUも食う」ということになりそうです。
なんてこった。
ただし、メモリを余計に食うのは Bitmap 生成時のみです。
BitmapFactory.createFromResourcesStream() などで Bitmap を生成する場合、 inScreenDensity などが inDensity と異なって設定されている場合、 一度原寸大で Bitmap を生成し、その Bitmap から Bitmap.createScaledBitmap() で density にあった scale された画像を作るようです。 原寸大の Bitmap はその後すぐ recycle() されていますが、原寸大と scale された両方の Bitmap がメモリ上にロードされることになります。
というわけで、画面密度にあったサイズの画像を用意した方が、メモリとCPUに優しいということになりそうです。
シェアが多い hdpi xhdpi あたりはカバーしといた方が良さそうです。
dashboard ではまだそうでもないですが、体感的には xxhdpi もかなり増えてる気がします。
アプリサイズとのにらみ合いが辛い今日この頃。