UnityでLINQソートを使う際の注意点

UnityでLINQソートを使う際の注意点

Unityの端末ビルドでLINQとIComparerを組み合わせるとフリーズしてしまう問題

参考URL
業務でUnity開発をしていて、端末ビルドしたところ、iOSでもAndroidでも完全にアプリがフリーズしてしまう現象が発生していた。
XCodeやAndroidStudioで端末ログを見ても、フリーズに繋がるようなエラーは確認できなかった。

細かくログを仕込んでいつフリーズが発生するか確認した所、これがLINQのOrderByソートで起きていたことが分かった。

LINQのフリーズした箇所のコード

結論から言うとLINQのOrderByを使った匿名関数内で、IComparer継承クラスを使っていたのが問題だった。
C#としては正しい使い方のはずで、Editor上では問題が起きてなかったのに、何故か端末ではフリーズしていた。

業務上のコードをそのまま書くと守秘義務違反になるので、ぼかして概ね似たようなコードを書いていく。
例えば下のようなデータクラスとICompaerer継承クラスがあったとして、
using System.Collections.Generic;

//インデックスと3つのステータスを持つシンプルなデータクラス

public class TestData
{
    public enum DataStatus
    {
        Uninitialized,
        Processing,
        Completed
    }

    private int idx;
    public int Idx => idx;
    private DataStatus status;
    public DataStatus Status => status;

    public TestData(int idx, DataStatus status)
    {
        this.idx = idx;
        this.status = status;
    }
}


//2つのTestDataを比較し、StatusがProcessingの者は手前に、Uninitializedは真ん中に、Completedは後ろにまとまる様にする

//その上でそれぞれのStatusグループをIdx順に並べる

public class TestComparer : IComparer<TestData>
{
    public int Compare(TestData d1, TestData d2)
    {
        if(d1.Status != d2.Status)
        {
            switch(d1.Status)
            {
                case TestData.DataStatus.Processing:
                    return -1;
                case TestData.DataStatus.Uninitialized:
                    return d2.Status == TestData.DataStatus.Processing? 1 : -1;                    
                case TestData.DataStatus.Completed:
                    return 1;
                default:
                    return 0;
            }
        }
        else
        {
            return d1.Idx > d2.Idx ? 1 : -1;
        }
    }
}
このTestDataに対し次のようなListを作り、ソートを実行したとする。
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class TestSort : MonoBehaviour
{
    private void Start()
    {
        List<TestData> dataList = new List<TestData>()
        {
            new TestData(0, TestData.DataStatus.Completed),
            new TestData(1, TestData.DataStatus.Uninitialized),
            new TestData(2, TestData.DataStatus.Completed),
            new TestData(3, TestData.DataStatus.Uninitialized),
            new TestData(4, TestData.DataStatus.Processing),
        };

        var sortedList = dataList.OrderBy(x => x, new TestComparer()).ToList();
    }
}

このOrdeByのコードでフリーズが起きていた。Editorでは正常に動くのに、何故かiOSとAndroidではフリーズする。
        var sortedList = dataList.OrderBy(x => x, new TestComparer()).ToList();

匿名関数内でnewをしたのが不味かったのかな?と思い分離してみたが、それでも結果は変わらなかった。
        var comparer = new TestComparer();
        var sortedList = dataList.OrderBy(x => x, comparer).ToList();

フリーズの解決

色々調べて、Unityが端末ビルドする際に使っていてる IL2CPP が、 LINQと相性悪いらしいという記事を見つけた。
しかしLINQ自体はOrderByも含めこれまでも散々使ってきていて、フリーズは起きていなかった。

今回初めてやったことと言えば、LINQとIComparerを組み合わせたことだったので、とりあえず半信半疑でLINQをListの標準Sortに置き換えてみた。
        
//var sortedList = dataList.OrderBy(x => x, new TestComparer()).ToList();

        var sortedList = new List<TestData>(dataList);
        sortedList.Sort(new TestComparer());  
するとiOSでもAndroidでもフリーズが解消されたのを確認できた。今でもなぞ。
0
0