VRMImporterContextはDisposeしないとメモリリークする

## TL;DR[^want]

- `VRM`をランタイムでロードするのに必要な`VRMImporterContext`は`IDisposable`を実装しており、`Dispose`しないとメモリリークする
- `VRMImporterContext`は生成済みのアバター`GameObject`についている`Component`からは取得できなさそう
- `VRM`のインスタンスを管理するときは`GameObject`ではなく`VRMImporterContext`で管理する
- もしくは[拡張メソッドで自作クラス](https://gist.github.com/nekomimi-daimao/8629d99e9b38bf00a68dad50c31543f4)をAddして`OnDestroy`のときに`Dispose`する

### 環境
[UniVRM v0.56.3](https://github.com/vrm-c/UniVRM/releases/tag/v0.56.3)

## 説明と検証

### 人間牧場

わたしは人間牧場を経営したいのですが、残念ながら資金も権力もありません。
なのでかわいいアバターをいっぱい飼育することで心の慰めにしたいと思います。
というわけで、以下のようなスクリプトを書きました。
`StreamingAssets/vrm/`から`.vrm`を読み込んでぴょんぴょんさせます。これで好きなアバターを牧場に入れられます！

```c#:HumanRanchそのいち

using System;
using System.Collections.Generic;
using System.IO;
using Cysharp.Threading.Tasks;
using UnityEngine;
using VRM;

public class HumanRanch : MonoBehaviour
{
    private void Start()
    {
        InitializeCage().ContinueWith(Rearing);
    }

    #region Ranch

    [SerializeField]
    private Transform[] _cage;

    [SerializeField]
    private RuntimeAnimatorController _animator;

    private GameObject[] _liveStocks;

    private async UniTask InitializeCage()
    {
        // wait for first update
        await UniTask.Yield(PlayerLoopTiming.Update);

        _liveStocks = new GameObject[_cage.Length];
    }

    private const float ProductionCycle = 3f;
    private readonly Vector3 CageGap = new Vector3(0, -1, 0);

    private async UniTask Rearing()
    {
        var token = this.GetCancellationTokenOnDestroy();
        while (!token.IsCancellationRequested)
        {
            var changeIndex = UnityEngine.Random.Range(0, _liveStocks.Length);
            if (_liveStocks[changeIndex] == null)
            {
                var context = await LoadVRM();
                var avatar = context.Root;
                avatar.GetComponent<Animator>().runtimeAnimatorController = _animator;
                avatar.transform.SetPositionAndRotation(
                    _cage[changeIndex].position + CageGap,
                    _cage[changeIndex].rotation
                );
                _liveStocks[changeIndex] = avatar;
                context.ShowMeshes();
            }
            else
            {
                Destroy(_liveStocks[changeIndex]);
                _liveStocks[changeIndex] = null;
            }

            await UniTask.Delay(TimeSpan.FromSeconds(ProductionCycle), cancellationToken: token);
        }
    }

    #endregion

    #region LoadVRM

    private readonly Dictionary<string, byte[]> _VRMBufferCache = new Dictionary<string, byte[]>();

    private string[] _VRMFullPaths = null;

    private static string[] SearchStreamingAssetsVRM()
    {
        var dirPath = Path.Combine(Application.streamingAssetsPath, "vrm");
        if (!Directory.Exists(dirPath))
        {
            Directory.CreateDirectory(dirPath);
        }

        return Directory.GetFiles(dirPath, "*.vrm");
    }

    private async UniTask<VRMImporterContext> LoadVRM()
    {
        if (_VRMFullPaths == null)
        {
            _VRMFullPaths = SearchStreamingAssetsVRM();
        }

        var path = _VRMFullPaths[UnityEngine.Random.Range(0, _VRMFullPaths.Length)];
        if (!_VRMBufferCache.ContainsKey(path))
        {
            await UniTask.SwitchToThreadPool();
            var buffer = File.ReadAllBytes(path);
            await UniTask.SwitchToMainThread();
            _VRMBufferCache[path] = buffer;
        }

        var context = await LoadVRMFromBuffer(_VRMBufferCache[path]);
        return context;
    }

    private static async UniTask<VRMImporterContext> LoadVRMFromBuffer(byte[] buffer)
    {
        var context = new VRMImporterContext();
        context.ParseGlb(buffer);
        await context.LoadAsyncTask();
        return context;
    }

    #endregion
}

```

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">人間牧場そのいち <a href="https://t.co/r6An34C7wd">pic.twitter.com/r6An34C7wd</a></p>&mdash; ねこみみだいまおう (@CatEarEvilKing) <a href="https://twitter.com/CatEarEvilKing/status/1284602809271283712?ref_src=twsrc%5Etfw">July 18, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

### 経営の行き詰まり

適度に増えて適度に減って、永遠に眺めていられますね。
ところがだんだん動きがカクついてきて、遂にはエディタがクラッシュしてしまいます。誰の仕業でしょうか。
`Profiler`で調べたところ、どうやらメモリリークしているようです。心当たりがあるとすれば破棄処理とファイル読み込みの部分ですが、特におかしいところはありません。

```c#:破棄処理
Destroy(_liveStocks[changeIndex]);
```

```c#:ファイル読み込み
var buffer = File.ReadAllBytes(path);
```

となると残っているのはロードするときに使っている`VRMImporterContext`です。
`IDisposable`を実装している。実にあやしい。というわけで使い終わったら`Dispose`します。

```c#
context.ShowMeshes();
// 即座にDispose
context.Dispose();
```

<img width="572" alt="スクリーンショット 2020-07-19 4.13.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/146245/81c037c3-ddb7-366a-8bc2-59435b048681.png">

そして後には誰もいない牧場が残りました。

### 経営再開

当然ですが、`context`を`Dispose`したタイミングで生成したアバターも破棄されてしまうみたいです。
そして困ったことに、生成済みのアバターは`VRMImporterContext`を取得できるクラスがくっついていません。[^seek]
なのでアバターを`GameObject`ではなく`VRMImporterContext`単位で管理することにします。

```c#

// アバターを管理する配列
// before
private GameObject[] _liveStocks;
// after
private VRMImporterContext[] _liveStocks;

// アバターの破棄処理
// before
Destroy(_liveStocks[changeIndex]);
// after
_liveStocks[changeIndex].Dispose();

```

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">人間牧場そのに <a href="https://t.co/Y5IYO52zwC">pic.twitter.com/Y5IYO52zwC</a></p>&mdash; ねこみみだいまおう (@CatEarEvilKing) <a href="https://twitter.com/CatEarEvilKing/status/1284603106378932224?ref_src=twsrc%5Etfw">July 18, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

できました！
人間牧場の復活です！
いろんなアバターが増えたり減ったりしながら永遠にぴょんぴょんします！


### 更にもう一歩
とはいえリソースの開放忘れはよくあるバグの原因の一つです。
なので管理して自分で`Dispose`するのではなく、
拡張メソッドでアバターが`OnDestroy`されたとき、自動的に`Dispose`されるようにします。

```c#:VRMAutoDisposer
using UnityEngine;

namespace VRM.Extension
{
    public class VRMAutoDisposer : MonoBehaviour
    {
        public VRMImporterContext Context;

        private void OnDestroy()
        {
            Context?.Dispose();
        }
    }

    public static class VRMAutoDisposerExtension
    {
        public static void AutoDispose(this VRMImporterContext context)
        {
            var disposer = context.Root.AddComponent<VRMAutoDisposer>();
            disposer.Context = context;
        }
    }
}

```

```c#
// 拡張メソッドなので、こんな感じで読んでおけば自動でDisposeしてくれる
context.AutoDispose();
context.ShowMeshes();
```

## まとめ

自分がざっと見た限りではそれらしい情報が見当たらなかったのでこの記事を書きました。
本来ならば公式にissueでも立てて要望を出すかなにかしようかとも思ったんですが、もうすぐ[1.0.0版](https://github.com/vrm-c/UniVRM_1_0)とかが出るみたいだし、いいかなって。
個人的には拡張メソッドの方式が好きです。`AssetBundle`でもそうですが、リソース管理とかめんどくさすぎて自分でやりたくないんですよね。

[拡張Dispose](https://gist.github.com/nekomimi-daimao/8629d99e9b38bf00a68dad50c31543f4)と[人間牧場](https://gist.github.com/nekomimi-daimao/a7794671214778008942f8d22d681b83)のコードは`gist`にあげておいたので好きに使ってください。
あと、`gist`に置いてあるコードの[ライセンス](https://gist.github.com/nekomimi-daimao/631f13b97667a411bfbb2fffe57acae9)をつけておきました[^sorry]。CC0ですが、作者を表記してくれたらうれしいです。

おしまい。

## 参考記事など

### 記事

[ランタイムインポーター](https://vrm.dev/dev/univrm-0.xx/programming/runtime_import/)

### 使用アセット

[ニコニ立体ちゃん (VRM)](https://3d.nicovideo.jp/alicia/)
[ヴィータ](https://hub.vroid.com/characters/6193066630030526355/models/3525604181073039892)
[Bird Cage](https://poly.google.com/view/0d4ExMmqXUV) created by [Poly by Google](https://poly.google.com/user/4aEd8rQgKu2)
[Basic Motions FREE Pack](https://assetstore.unity.com/packages/3d/animations/basic-motions-free-pack-154271)

[UniTask v2](https://github.com/Cysharp/UniTask)

[^want]: やってみたかったやつ。
[^seek]: もしあったら教えて下さい。
[^sorry]: 昔すぎて今更リプライするのもあれなのでこの場で謝ります。ごめんなさい。