Edited at

Android >=4.0時代はInternal Storageにキャッシュを置いて、自分で容量を調節する(Glideのサンプル付き)

そもそも6.0を目前に4.0の話するのかよってツッコミがありそうですがw


TL;DR


  • 以前Internal Storageの容量が小さかった頃に、大きなデータはExternal Storageに置けとか、Internalのキャッシュは1MB程度にしろとか言われていた

  • External Storageへの配置はセキュリティリスクやアンマウントの扱いに難がある

  • SDカード前提だったが、Internal Storageの容量が飛躍的に伸びた結果その必要がなくなった

  • 他のアプリと共有したい情報以外は、データ、キャッシュともにInternal Storageを使う

  • ただし古い端末の容量は小さいので、キャッシュの容量が大きくなる可能性がある場合は、端末のInternal Storageサイズに応じて制限する

  • モダンな端末は8GB (=7.45GiB)はあると見て良さそう

  • Internal StorageのサイズはStatFsを使うと測れるが、Robolectric上ではNPEで動かないので注意

  • GlideModule書いた


そもそもInternal StorageとExternal Storageって?

Androidでファイルを保存するためのストレージはInternalとExternalの2つあります。

http://developer.android.com/guide/topics/data/data-storage.html


Internal Storage

端末内部の取り出し不能なストレージ。デフォルトで、他のアプリ(やユーザ)からは参照できないようになっています。

特別な意図がなければこれを使っておいたほうが良いのですが、サイズが小さいから大きいファイルはExternalに置いたほうが良いのでは、という話がかつてありました。


External Storage

外部ストレージという概念()。他のアプリからやUSB経由で参照可能になっていて、使うだけでセキュリティリスクがあります。取り出し可能なSDカードのことがあり、SDが取り出されることを前提にアプリを作る必要があります。

最近は端末内ストレージの大容量化にともない、Internal Storageと同じ場所に同居している(emulated)ことが多いです。

※取り出し可能かはisExternalStorageRemovable()で、Internal Storageと同居しているかはisExternalStorageEmulated()で判定できます。


External Storageキャッシュ神話

どこからともなく、「大きいサイズのキャッシュは外部ストレージへ」みたいな話がちらほら聞こえてきたりします。

例えば、Androidの公式ドキュメントにはかなりはっきりとそれが書かれています。

http://developer.android.com/reference/android/content/Context.html#getCacheDir()


you should always have a reasonable maximum, such as 1 MB, for the amount of space you consume with cache files

訳: キャッシュファイルで利用する領域は、1MB程度の適切なサイズに必ず制限するべき


もし本当にExternal Storage上にキャッシュを置くなら暗号化するべきだということが、Android セキュアコーディングガイドにも載っています。Facebookは、SDカードにFacebook上の画像のキャッシュを暗号化して保存するために、concealという暗号化ライブラリを作っていたりします。

しかし、1MBなどという制限は最近のAndroid端末の性質を反映しきれていないのです。


内部ストレージ1GB→8GBの壁

Android 2.x時代には、SDカードを前提としてROMが256MB〜1GBとかの端末がゴロゴロいました(Xperia acroに泣かされたのは良い思い出・・)。

一方で、WikipediaのAndroid端末一覧や、ARROWSXperiaの機種一覧を見ると、2011年の端末はROMが1GBのものがちらほらありますが、2012年の端末はほとんどが8GB以上になっています(2012年はAndroid 4.0が端末の初期OSとして採用された年でもあるようです)。またAndroid 4.0にアップデートされた端末の多くが4GB/8GBのROMを持っています。

例えばXperiaでは2012〜2014年のローエンド?端末に4GBのものがいくつかありますが、端末数はわずかに見受けられます(調べたわけじゃない)。(ちなみにGalaxyはちゃっかり初代S(2010年)から16GB積んでましたw)

これらのことから、モダン端末は8GB (=7.45GiB)以上のROMを積んでいると仮定して大丈夫かな、と思います。

これなら、数十MBくらい使っても余裕で持ちこたえられます。

(もしかしたら、後述のGlideのように100MB単位で使いたい場合は16GBぐらい見ておいたほうが良いのかも・・?)


モダンな画像ローダの仕様

最近流行っている画像ローダライブラリはInternal Storageをせっせと使っています。

GlideにはExternal Storageのサポートが比較的最近になって追加されましたが、ちゃんと使うには取り出しも管理しないといけないはずなので、このコードのまま使うのは難しいかなと思いました。


Internal Storageのサイズの測り方

2.x時代からAndroidエンジニアな先輩方(@hidey さん、@punchdrunker さんありがとうございます!)に相談したところ、Internal Storageのサイズを自前で測ってキャッシュサイズを調整するのがよいだろう、ということになりました。

AndroidのクラスであるStatFsを使うことで取得できます。

StatFsのインスタンスは new StatFs(Environment.getDataDirectory().getPath()) のように得ます。

参考: http://stackoverflow.com/a/4595449/1474113

ただし、便利な(というか最も直感的な・・・)メソッドの追加と引き換えにdeprecatedになったメソッドがあり、Android 4.3未満の端末ではこれを参照しなくてはならないです。

    private long getTotalBytesOfInternalStorage() {

// http://stackoverflow.com/a/4595449/1474113
StatFs stat = new StatFs(Environment.getDataDirectory().getPath());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
return getTotalBytesOfInternalStorageWithStatFs(stat);
} else {
return getTotalBytesOfInternalStorageWithStatFsPreJBMR2(stat);
}
}

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
private long getTotalBytesOfInternalStorageWithStatFs(StatFs stat) {
return stat.getTotalBytes();
}

@SuppressWarnings("deprecation")
private long getTotalBytesOfInternalStorageWithStatFsPreJBMR2(StatFs stat) {
return (long) stat.getBlockSize() * stat.getBlockCount();
}

なお、StatFsはRobolectric 3.0でテスト中に参照すると、内部でNPEエラーが出てしまうので、Unit Testの際は参照しないように工夫する必要があります。


測ったサイズに応じてGlideのキャッシュサイズを制限するコード

GlideModuleの実装はこんな感じになります(AndroidManifestに書かないと動かないので注意)。

https://gist.github.com/ypresto/1ae178b2f53ed70b7ac1