Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9353

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

$
0
0

TL;DR1

  • VRMをランタイムでロードするのに必要なVRMImporterContextIDisposableを実装しており、Disposeしないとメモリリークする
  • VRMImporterContextは生成済みのアバターGameObjectについているComponentからは取得できなさそう
  • VRMのインスタンスを管理するときはGameObjectではなくVRMImporterContextで管理する
  • もしくは拡張メソッドで自作クラスをAddしてOnDestroyのときにDisposeする

環境

UniVRM v0.56.3

説明と検証

人間牧場

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

HumanRanchそのいち
usingSystem;usingSystem.Collections.Generic;usingSystem.IO;usingCysharp.Threading.Tasks;usingUnityEngine;usingVRM;publicclassHumanRanch:MonoBehaviour{privatevoidStart(){InitializeCage().ContinueWith(Rearing);}#regionRanch[SerializeField]privateTransform[]_cage;[SerializeField]privateRuntimeAnimatorController_animator;privateGameObject[]_liveStocks;privateasyncUniTaskInitializeCage(){// wait for first updateawaitUniTask.Yield(PlayerLoopTiming.Update);_liveStocks=newGameObject[_cage.Length];}privateconstfloatProductionCycle=3f;privatereadonlyVector3CageGap=newVector3(0,-1,0);privateasyncUniTaskRearing(){vartoken=this.GetCancellationTokenOnDestroy();while(!token.IsCancellationRequested){varchangeIndex=UnityEngine.Random.Range(0,_liveStocks.Length);if(_liveStocks[changeIndex]==null){varcontext=awaitLoadVRM();varavatar=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;}awaitUniTask.Delay(TimeSpan.FromSeconds(ProductionCycle),cancellationToken:token);}}#endregion#regionLoadVRMprivatereadonlyDictionary<string,byte[]>_VRMBufferCache=newDictionary<string,byte[]>();privatestring[]_VRMFullPaths=null;privatestaticstring[]SearchStreamingAssetsVRM(){vardirPath=Path.Combine(Application.streamingAssetsPath,"vrm");if(!Directory.Exists(dirPath)){Directory.CreateDirectory(dirPath);}returnDirectory.GetFiles(dirPath,"*.vrm");}privateasyncUniTask<VRMImporterContext>LoadVRM(){if(_VRMFullPaths==null){_VRMFullPaths=SearchStreamingAssetsVRM();}varpath=_VRMFullPaths[UnityEngine.Random.Range(0,_VRMFullPaths.Length)];if(!_VRMBufferCache.ContainsKey(path)){awaitUniTask.SwitchToThreadPool();varbuffer=File.ReadAllBytes(path);awaitUniTask.SwitchToMainThread();_VRMBufferCache[path]=buffer;}varcontext=awaitLoadVRMFromBuffer(_VRMBufferCache[path]);returncontext;}privatestaticasyncUniTask<VRMImporterContext>LoadVRMFromBuffer(byte[]buffer){varcontext=newVRMImporterContext();context.ParseGlb(buffer);awaitcontext.LoadAsyncTask();returncontext;}#endregion}

経営の行き詰まり

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

破棄処理
Destroy(_liveStocks[changeIndex]);
ファイル読み込み
varbuffer=File.ReadAllBytes(path);

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

context.ShowMeshes();// 即座にDisposecontext.Dispose();

スクリーンショット 2020-07-19 4.13.24.png

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

経営再開

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

// アバターを管理する配列// beforeprivateGameObject[]_liveStocks;// afterprivateVRMImporterContext[]_liveStocks;// アバターの破棄処理// beforeDestroy(_liveStocks[changeIndex]);// after_liveStocks[changeIndex].Dispose();

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

更にもう一歩

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

VRMAutoDisposer
usingUnityEngine;namespaceVRM.Extension{publicclassVRMAutoDisposer:MonoBehaviour{publicVRMImporterContextContext;privatevoidOnDestroy(){Context?.Dispose();}}publicstaticclassVRMAutoDisposerExtension{publicstaticvoidAutoDispose(thisVRMImporterContextcontext){vardisposer=context.Root.AddComponent<VRMAutoDisposer>();disposer.Context=context;}}}
// 拡張メソッドなので、こんな感じで読んでおけば自動でDisposeしてくれるcontext.AutoDispose();context.ShowMeshes();

まとめ

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

拡張Dispose人間牧場のコードはgistにあげておいたので好きに使ってください。
あと、gistに置いてあるコードのライセンスをつけておきました3。CC0ですが、作者を表記してくれたらうれしいです。

おしまい。

参考記事など

記事

ランタイムインポーター

使用アセット

ニコニ立体ちゃん (VRM)
ヴィータ
Bird Cage created by Poly by Google
Basic Motions FREE Pack

UniTask v2


  1. やってみたかったやつ。 

  2. もしあったら教えて下さい。 

  3. 昔すぎて今更リプライするのもあれなのでこの場で謝ります。ごめんなさい。 


Viewing all articles
Browse latest Browse all 9353

Latest Images

Trending Articles