【Unity】AssetBundleの個別削除に関する覚書【AssetBundle】

# <font color="red">2017/7/12 追記</font>
Unity 2017.1で [Caching](https://docs.unity3d.com/ScriptReference/Caching.html) クラスのAPIがいろいろ変わってキャッシュを個別管理できるようになったみたいです！キャッシュの削除メソッドが同期的に実行されるのは変わってないようなので次のバージョンに期待。

# はじめに
[`WWW.LoadFromCacheOrDownload`](https://docs.unity3d.com/ScriptReference/WWW.LoadFromCacheOrDownload.html)やUnityWebRequestの`DownloadHandlerAssetBundle`を使用するとAssetBundleがキャッシュされます。キャッシュされたAssetBundleは[`Caching`](https://docs.unity3d.com/ScriptReference/Caching.html)クラスのAPIを通じて操作できますが、キャッシュを直接的に削除する方法は[`Caching.CleanCache`](https://docs.unity3d.com/ScriptReference/Caching.CleanCache.html)による全削除しか提供されていません（Unity5.5現在）。

また、何も考えずに上記のダウンロードAPIを使用すると古いAssetBundleがキャッシュされたままになり、モバイルの場合はアプリの容量にかなりの悪影響が出るものと考えられます。

このエントリーでは上記のような問題について、iOSやAndroidのネイティブコードを書かずに済ませる実装を考えるために参考になりそうなことをまとめておきます。

# キャッシュの削除の仕方

## APIによる全削除

[`Caching.CleanCache`](https://docs.unity3d.com/ScriptReference/Caching.CleanCache.html)を利用すると問答無用ですべてのAssetBundleのキャッシュが削除されます。
しかも同期的に処理が行われるので、モバイル端末のスペックとキャッシュの量によっては５分くらい画面が固まって操作できなくなります。

## 有効期限切れによる削除

[`Caching.expirationDelay`](https://docs.unity3d.com/ScriptReference/Caching-expirationDelay.html)の値をセットしておくと、有効期限が切れたキャッシュは削除されるようです（デフォルトでは150日=12,960,000秒）。

また、[`Caching.MarkAsUsed`](https://docs.unity3d.com/ScriptReference/Caching.MarkAsUsed.html)を利用することでAssetBundleを個別で指定してキャッシュのタイムスタンプを現在時間に更新する（=有効期限を伸ばす）ことができるようです。

「未使用の状態がexpirationDelay秒続いたら期限切れ」のようなのですが、詳細な条件はよく調べておらず不明。知ってる方いたら教えて下さい。

[`Caching.MarkAsUsed`](https://docs.unity3d.com/ScriptReference/Caching.MarkAsUsed.html)のシグネチャは

`public static bool MarkAsUsed(string url, int version);`
`public static bool MarkAsUsed(string url, Hash128 hash);`（リファレンスに載ってない）

の２つで時間指定などはできません。有効期限切れで削除されるタイミングが有効期限が切れた瞬間とかだと若干使いづらそうです。

## OSによる自動削除

ソースが見当たらないのですが、昔はiOSによって勝手に削除される可能性があるフォルダにキャッシュが置かれていた記憶があります（Application.temporaryCachePathだとそうなるはずでそこに保存だったような、嘘だったら指摘ください）。

[Unity5.3.2, 5.3.3系でAssetBundleがiCloudに入っちゃう暫定対策[fixed]](http://sassembla.github.io/Public/2016:02:19%2017-17-27/2016:02:19%2017-17-27.html)
[[Unity][Unity3d] AssetBundle が iCloud のバックアップ対象と成っている問題の対象方法](http://d.hatena.ne.jp/nakamura001/20160220/1455975339)

推測ですが、上記のエントリーで紹介されている不具合が発生したバージョンあたりからキャッシュ先が変更され、現在はiOSによって勝手に削除はされないものと思われます。[マニュアル](https://docs.unity3d.com/jp/current/Manual/iphone-Downloadable-Content.html)にも

> もしより細かいところまで AssetBundle のキャッシュファイルを制御しないといけない場合、WWW ダウンロードを LoadFromCacheOrDownload ではなく new WWW(url) 経由で使用して、.NET ファイル API を使用してダウンロードされたファイルをディスク上で格納することができます。必要なファイルを Application.temporaryCachePath (OS によりクリーンされる Library/Caches) フォルダーや Application.persistentDataPath (OS によりクリーンされない Documents フォルダー) に保存することができます。これらのファイルの No Backup フラグを iOS.​​Device.SetNoBackupFlag 経由で有効化すべきであり、iCloud にバックアップされることを防止すべきです。

と書いてあるので少なくともUnity5.4以降ではiOSには消されないでしょう。Androidは要確認。

# AssetBundleのキャッシュに関する挙動

以下の記事に詳しくまとまっています。

* [いまさら書けないAssetBundleのcache周りの知見](http://sassembla.github.io/Public/2015:02:04%2012-47-46/2015:02:04%2012-47-46.html)

要約すると

* 同じAssetBundle名
* 同じバージョン値
* 異なるCRC

の条件を満たすAssetBundleをダウンロードすると同名の古いAssetBundleが消えるということみたいです。
逆に上記を満たしていない場合は古いAssetBundleがキャッシュに残ったままになる、と。

ちなみにドキュメントには載ってませんが[` WWW.LoadFromCacheOrDownload`](https://docs.unity3d.com/ScriptReference/WWW.LoadFromCacheOrDownload.html)には別のオーバーロードが提供されており、

`public static WWW LoadFromCacheOrDownload(string url, Hash128 hash, uint crc = 0);`

というシグネチャになっています。

ドキュメントに載っているversion値を使用するほうは内部的には上記のオーバーロードに呼び出しを委譲しているようです。

# キャッシュの存在問い合わせ

AssetBundleがキャッシュされているかどうかは[`Caching.IsVersionCached`](https://docs.unity3d.com/ScriptReference/Caching.IsVersionCached.html)で問い合わせできます。シグネチャは以下の通り。

`public static bool IsVersionCached(string url, int version);`
` public static bool IsVersionCached(string url, Hash128 hash);`（リファレンスに載ってない）

先述の挙動を利用した上書き更新を行うためにAssetBundleのバージョン値（もしくはhash値）を固定するとcrc以外変化する値がないので「すでにダウンロード済みだが再度ダウンロードさせて上書きしたい」という場合のキャッシュ存在判定には`Caching.IsVersionCached`を使用することはできません。

urlとversion（hash）とcrcの３つを引数として取るオーバーロードがあれば良かったのですが・・・。

先述の「iOSで勝手にキャッシュが消される」が心配ないならダウンロード時にキャッシュリストを作成/更新してそちらを参照するのもありかも。

# 問題点のまとめ

キャッシュされたAssetBundleは

* 個別に指定しての削除はできない（全削除しかできない）
* Unityの想定通りの使い方だと新しいAssetBundleをダウンロードしても古いAssetBundleは消えない
* 新しいAssetBundleで上書き更新する場合はキャッシュ状況を問い合わせできない（ダウンロード試行必須）

# キャッシュの個別削除の実装の検討

## 誤ったcrcを指定する

[【Unity】AssetBundleについて調べたメモ](http://lycoris102.hatenablog.com/entry/2016/07/13/092540)によると

> CRCを常に違う値を渡すことでUnity5.3未満では削除される挙動となっていた
>    このアプローチはQiita等で頻繁に見かけるが、5.3ではこのハックは使えなくなった

ということですでに使えない様子。

## 最大キャッシュサイズを適切に指定して古いキャッシュを削除する

先述の「[`いまさら書けないAssetBundleのcache周りの知見`](http://sassembla.github.io/Public/2015:02:04%2012-47-46/2015:02:04%2012-47-46.html)」に書いてある方法。

> 
・新規取得するAssetBundleと、残したいAssetBundleを把握できるようにする
・残したいAssetBundleに参照マークを付ける
・新規取得するAssetBundleの展開後サイズと、残したいAssetBundleの解凍後サイズを合算した値をCaching.maximumAvailableDiskSpaceにセット(ムズイ)
これで、
新規をDLすれば、最近参照マークがついていない(= 最終参照が古い)ものからゴウンゴウン消えて行く。

AssetBundleはダウンロード後に圧縮した状態で保持できるようになったので割りと現実的なやり方かなと思いきや、[`Caching.maximumAvailableDiskSpace`](https://docs.unity3d.com/ScriptReference/Caching-maximumAvailableDiskSpace.html)はリファレンス見る限りWebPlayer用ぽいので無理そう（getterがおかしな値を返すだけでsetterは問題ない？要確認）。

## 空のAssetBundleで上書きする

0. 消したいAssetBundleと同名でなるべく容量を小さくしたダミーAssetBundleを作成する
0. ダミーAssetBundleをダウンロードさせて上書きさせる

「完全に消す」のではなく「なるべく小さいもので上書きする」というアプローチです。

* version（hash）は固定する必要があるので更新時のキャッシュの存在判定に`Caching.IsVersionCached`は使えない
  * 予めキャッシュのリストを作成しておく？
* どれくらい容量が小さいAssetBundleを作成できるかわからない（やってみれば分かる話ですが・・・）
* 完全に消すわけではないので不都合ありそう

## 有効期限を適切に指定する

0. [`Caching.MarkAsUsed`](https://docs.unity3d.com/ScriptReference/Caching.MarkAsUsed.html)で残したいキャッシュのタイムスタンプ更新
0. [`Caching.expirationDelay`](https://docs.unity3d.com/ScriptReference/Caching-expirationDelay.html)で有効期限を5秒とか指定
0. 残したいキャッシュ以外は削除される（はず。タイミングは不明）
0. [`Caching.expirationDelay`](https://docs.unity3d.com/ScriptReference/Caching-expirationDelay.html)の値を元に戻す

というやり方で古いキャッシュのみ消せそうです。

# たくさん書いて疲れた

Unityさん個別削除APIください。