【Unity】WWW.LoadFromCacheOrDownloadでダウンロードが発生するとファイルが開きっぱなしになる【AssetBundle】

Unityが提供しているAssetBundleキャッシングシステムAPIである`WWW.LoadFromCacheOrDownload()`を使うとどう頑張ってもファイルが開放できなかったので検証記録を残します。

ちなみに「メモリが開放されない」ではなく「ファイルが開きっぱなしになって閉じれない」問題です。

# <font color="red">2016/1/30追記</font>
Unity5.3.2f1のエディタ上では相変わらず起きてますが5.4.0b3のエディタでは起きなかったので修正されてるっぽいです。


#検証に使った環境
・Mac（OS X 10.9.5）
・Unity 5.3.0f4
・ビルドターゲット：iOS

UnityエディタとiOSの実機で起きる模様。WindowsとAndroidの実機では起きてません。
また、この現象自体はUnity5.0くらいからずっと出てた気がします。

#条件と症状
##条件
いろいろ試して判明した条件は
・`WWW.LoadFromCacheOrDownload()`を呼んで
・キャッシュにヒットせずにダウンロードが発生した場合
です。

##症状
・950個くらいダウンロードした時点でUnityエディタが固まる
（iOSアプリとしてビルドしたときも実機で同じことが起こる？）

##原因
・ダウンロードしたファイルが開きっぱなしになる
・開きっぱなしのファイルが増え、１プロセスで開けるファイルの上限に達すると死ぬ？

ちなみに以下は試してみましたが効果がありませんでした（メモリしか開放してないっぽい）。
・`WWW#Dispose()`
・`AssetBundle#Unload(bool)`
・`Resources.UnloadUnUsedAssets()`

#問題についてググった結果
それらしい日本語の情報はありませんでしたが、海外のUnityフォーラムに似たようなことを言ってる人がいました。

[Problem when the ios build download AssetBundle via WWW.LoadFromCacheOrDownload](http://forum.unity3d.com/threads/problem-when-the-ios-build-download-assetbundle-via-www-loadfromcacheordownload.282487/)

↑のやり取りにおけるUnityの中の人曰く、
> In your code, after you have downloaded the file, you should then load the assetbundle with .assetBundle, load the assets you need from it using AssetBundle.Load() and then unload the ones that you don't need with AssetBundle.Unload (false) and finally dispose of the WWW object with WWW.Dispose(). 

「ファイルをダウンロードしたら`.assetBundle`でAssetBundleをロードして`Load()`でアセットをロード、そしたら`Unload(false)`でアンロードして、最後は`WWW.Dispose()`で`WWW`オブジェクトを破棄する必要がある」
だそうです。これらのようにやってもファイルは閉じることができませんでした。

#再現手順

## AssetBundleを用意
数があればいいので適当に小さなテキストファイルを1,000個ほど用意。
以下のスクリプトを使ってiOS用とAndroid用をビルド。

・(プロジェクトのパス)/AssetBundles/iOS/
・(プロジェクトのパス)/AssetBundles/Android/
にAssetBundleが吐き出されます。

```csharp:AssetBundleBuildScript
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;

public class AssetBundleBuildScript
{
	private const string ASSET_BUNDLE_TARGET_PATH = "AssetBundleTargets";

	[MenuItem ("Assets/AssetBundle Test/Create Text Files")]
	static void CreateTextFiles ()
	{
		var count = 1000;

		if (Directory.Exists (Application.dataPath + "/" + ASSET_BUNDLE_TARGET_PATH) == false) {
			Directory.CreateDirectory (Application.dataPath + "/" + ASSET_BUNDLE_TARGET_PATH);
			AssetDatabase.Refresh ();
		}

		for (var i = 1; i < count + 1; i++) {
			var name = string.Format ("{0:0000}", i);
			var path = "Assets/" + ASSET_BUNDLE_TARGET_PATH + "/" + name + ".txt";
			File.WriteAllText (path, name, Encoding.UTF8);
		}
		AssetDatabase.Refresh (ImportAssetOptions.ImportRecursive);
	}

	[MenuItem ("Assets/AssetBundle Test/Build AssetBundles For iOS")]
	static void BuildAssetBundlesForiOS ()
	{
		BuildAssetBundles (BuildTarget.iOS);
	}

	[MenuItem ("Assets/AssetBundle Test/Build AssetBundles For Android")]
	static void BuildAssetBundlesForAndroid ()
	{
		BuildAssetBundles (BuildTarget.Android);
	}


	static void BuildAssetBundles (BuildTarget buildTarget)
	{
		var targetGUIDs = AssetDatabase.FindAssets ("", new string[] { "Assets/" + ASSET_BUNDLE_TARGET_PATH });
		var buildList = new List<AssetBundleBuild> ();

		foreach (var guid in targetGUIDs) {
			var path = AssetDatabase.GUIDToAssetPath (guid);
			var build = new AssetBundleBuild ();
			build.assetBundleName = Path.GetFileNameWithoutExtension (path);
			build.assetNames = new string[] { path };
			buildList.Add (build);
		}

		if (Directory.Exists (Application.dataPath + "/../AssetBundles/" + buildTarget.ToString ()) == false) {
			Directory.CreateDirectory (Application.dataPath + "/../AssetBundles/" + buildTarget.ToString ());
			AssetDatabase.Refresh ();
		}

		BuildPipeline.BuildAssetBundles (
			"AssetBundles/" + buildTarget.ToString (),
			buildList.ToArray ()
		);

		AssetDatabase.Refresh ();
		Debug.Log ("build done!");
	}
}
```

## AssetBundleをダウンロードできるようにする
Unityエディタからダウンロードできるように適当にサーバ立ち上げて`localhost:8000/(アセットバンドル名)`でアクセスできるようにします。

```bash
$ cd (Unityのプロジェクトのパス)/AssetBundles/iOS
$ ruby -rwebrick -e 'WEBrick::HTTPServer.new(:DocumentRoot => "./", :Port => 8000).start'
```

##Unityエディタが開いているファイル数を確認する
`lsof`コマンドで確認。何度も打つのが面倒なのでhomebrewで`watch`を入れます。

```bash
$ brew install watch
$ watch -n 0.1 'ps aux | grep "MacOS\/Unity" | grep -v Helper | tr -s " " | cut -d " " -f 2 | xargs lsof -p | wc -l'
```

自分の環境だとUnityで検証用プロジェクトを開くと170くらいの数値で安定。

##適当なシーンを作ってダウンロードしてみる
以下のスクリプトで検証。
キャッシュにヒットしないように`CleanCache()`を呼んでから`DownloadAll()`を呼びます。Buttonとかに適当に貼り付けてください。

```c#:Test01
using UnityEngine;
using System.Collections;

public class Test01 : MonoBehaviour
{
	public void CleanCache ()
	{
		Caching.CleanCache ();
		Debug.LogWarning ("cache cleaned!");
	}

	public void DownloadAll ()
	{
		StartCoroutine (DownloadAllCoroutine ());
	}

	private IEnumerator DownloadAllCoroutine ()
	{
		while (Caching.ready == false) {
			yield return null;
		}

		for (var i = 1; i < 1001; i++) {
			var name = string.Format ("{0:0000}", i);
			yield return StartCoroutine (DownloadCoroutine (name));
		}
	}

	private IEnumerator DownloadCoroutine (string name)
	{
		var url = "http://localhost:8000/" + name;

		Debug.Log ("Download Start : " + url);

		using (var www = WWW.LoadFromCacheOrDownload (url, 1, 0)) {
			yield return www;

			if (string.IsNullOrEmpty (www.error) == false) {
				Debug.Log (www.error);
				yield break;
			}

			Debug.LogWarning ("Download Done : " + url);

			var assetBundle = www.assetBundle;
			var asset = assetBundle.LoadAsset (name) as TextAsset;

			Debug.Log ("inner text : " + asset.text);

			assetBundle.Unload (true);
		}
	}
}
```

#結果
950個目くらいのAssetBundleをダウンロードしたときに以下のようなエラーが出ます。

`Unable to open archive file for writing: '/Users/su10/Library/Caches/Unity/Temp/4fa2d7aae719d402f81fe16b41e285ed/__data'`
`Failed to decompress data for the AssetBundle 'http://localhost:8000/0952'.`
`***Thread '(null)' tried to join itself!***`
`Error joining threads: 22`
`!m_Running && "Thread shouldn't be running anymore"`
`m_ArchiveConverter must be created!`

`lsof`での監視だと1120の値の時点で止まってます。

#最後に
「再現した」とか「コードがおかしい」などご指摘あればよろしくお願いします。