概要
ある GameObject の Texture を毎フレーム更新して動画のように描画するコードを書いていたところ、ネイティブ環境で異常にクラッシュする事象が発生した。その時に調査した手法と原因をまとめておく。
調査手法
下記のようなエラーを事前に吐いていたので、メモリ関連であるということは分かっていた。 (WebGL)
Uncaught RangeError : Maximum Call Stack Size Exceeded
次に Profiler からメモリ使用量を確認すると、Unity メモリが 3GB を超えていることを確認。(上昇ペースも1秒に数十MBだった)
更に、メモリビューの Detailed から何がメモリを食っているのか確認すると、画像のように Texture2D が毎フレームごとに 2.3MB メモリ確保していることが判明した。
Texture2D をこんなに生成している箇所は 1 箇所しか心当たりが無かったので、該当コードを確認することにした。
原因
該当コードは下記の通り。
// frameはtexture情報をバイナリで保持している独自クラス
var texture2D = new Texture2D(frame.Width, frame.Height, TextureFormat.RGBA32, false);
texture2D.wrapMode = TextureWrapMode.Clamp;
texture2D.LoadRawTextureData(frame.Buffer);
texture2D.Apply();
material.mainTexture = texture2D;
ここで問題になっているのは new Texture2D()
の部分で、今回確保されたメモリは今フレームでは material.mainTexture
が参照するが、material.mainTexture
は次フレームでは別の参照を保持している。そのため、前フレームに確保した Texture2D
のメモリ領域はどこからも参照されなくなり、Unity メモリなので GC も行われずそのままリークするといった事が原因であった。
解決策
前フレームで使用していた Texture2D
のメモリを手動で解放する事で解決。
Unity で利用するアセットのメモリ解放は MonoBehaviour.Destroy()
で出来る。
実際の修正済みコードは下記の通り。
var texture2D = new Texture2D(frame.Width, frame.Height, TextureFormat.RGBA32, false);
texture2D.wrapMode = TextureWrapMode.Clamp;
texture2D.LoadRawTextureData(frame.Buffer);
texture2D.Apply();
// これを追加した
Destroy(material.mainTexture);
material.mainTexture = texture2D;
これで無事にメモリリークは解消され、ネイティブ環境でアプリがクラッシュすることも無くなった。
まとめ
「アプリが重い、メモリ関係でクラッシュする」といった場合はまず「Profiler」を確認すること。
闇雲にあたりを付けて修正してもコストが掛かるだけで改善するとは限らない。
Profiler を使った最適化は 「【Unite 2017 Tokyo】最適化をする前に覚えておきたい技術」の動画を見たメモ にまとめてあるので、全く縁が無かった方は読んでおくと良いと思われる。
※ 調査方法や解決方法で更に効率的な方法などがあればコメントを頂けますと幸いです。