UniRx의 사용법을 잘못하여 동결을 일으켰습니다.

7958 단어 UniRxC#Unity

소개



초보적인 실수를 해 버렸기 때문에, 다시 발생시키지 않게 하기 위해서도 비망록을 남깁니다.

제작하고 있는 앱에서 일정 시간 메인 스레드가 멈추고 표시가 갱신되지 않는 프리즈가 특정 조건에서 발생하고 있었습니다.

원인을 조사하기 위해 프로파일 러를 보았는데, GarbageCollection에서 큰 산이 😱


GC가 발생한 부분



프로파일 러의 세부 사항을 보면 ObservableDestroyTrigger.OnDestroy에서 발생했습니다.

코드를 살펴보면 IObservable의 Subscribe시에 호출하는 AddTo()가 원인이었습니다.

문제가 된 코드



실제 코드는 올려지지 않기 때문에 가까운 거동을 하는 코드가 됩니다.
using System;
using UniRx;
using UnityEngine;

public class Sample : MonoBehaviour {
    private Subject<Unit> _apiSubject = new Subject<Unit>();
    private Subject<Unit> _subject = new Subject<Unit>();

    void Start() {
        // ApiReceive時の想定
        _apiSubject.StartWith(Unit.Default).Subscribe(_ => {
            UpdateSubject();
        }).AddTo(this);

        // Apiの複数回投げたときの想定
        for (int i = 0; i < 26000; i++) {
            _apiSubject.OnNext(Unit.Default);
        }
    }

    void UpdateSubject() {
        _subject.Subscribe(_ => {
            Debug.Log("DoSomething");
        }).AddTo(this);
    }

    void Update() {
        if (Input.GetKeyDown(KeyCode.D)) {
            Destroy(gameObject);
        }
    }
}


실제 코드가 Api인 데이터의 응답이 돌아왔을 때에 대상의 처리를 재구독한다고 하는 것이었으므로, 위와 같은 코드가 되고 있습니다.

(실제 코드에서는 이렇게 많이 Next를 발행하지 않지만 실제로 일어난 거동을 재현하기 위해 이렇게 하고 있습니다)

그건 그렇고,이 코드에서 GameObject가 Destroy되면 2.52GB GC Alloc이 작동합니다.

왜 AddTo가 필요한가?



구독과 오브젝트를 연결하지 않으면 오브젝트가 파기되어도 구독이 남아 처리가 실행되어 버립니다.

이번 코드는 같은 클래스내에서 생성한 Subject에 대해서 구독하고 있으므로, AddTo()가 없어도 문제 없습니다만,
클래스를 넘을 때 문제가 나오므로 AddTo를 호출하는 버릇을 붙여 두는 것이 좋습니다.

실제 거동



동결을 쉽게 알 수 있도록 애니메이션 큐브를 표시합니다.



D 키를 누를 때 GameObject가 Destroy 될 때 동결이 발생합니다.

무엇이 문제였는가



이 처리에 문제가 있습니다.
    void UpdateSubject() {
        _subject.Subscribe(_ => {
            Debug.Log("DoSomething");
        }).AddTo(this);
    }

다시 구독할 때에, 전회 구독하고 있던 것을 파기하고 있지 않기 때문에 점점 구독이 쌓여 가 버립니다.

올바른 코드


    private IDisposable _disposable = null;
    void UpdateSubject() {
        _disposable?.Dispose();
        _disposable = _subject.Subscribe(_ => {
            Debug.Log("DoSomething");
        }).AddTo(this);
    }

재구독 시 위와 같이 이전의 것을 파기한 후 구독합니다.

?는 null 조건 연산자이며 객체가 null이면 함수를 호출하지 않습니다.
다음과 같은 처리가 됩니다.
        if (_disposable != null) {
            _disposable.Dispose();
        }

마지막으로



평소, 재구독할 때는 전회 구독한 것을 파기한다고 조심하고는 있었습니다만, 이번은 실수로 올바르지 않은 코드가 되어 있었습니다.

약간의 실수가 알기 어려운 문제를 낳습니다.

이번 실수 덕분에 GC가 대량으로 달려 버리는 패턴을 발견할 수 있었습니다.

이번 소개한 내용은 초보적인 실수입니다만, 앞으로는 AddTo에 같은 오브젝트를 대량으로 지정하지 않도록 주의해야 합니다.

좋은 웹페이지 즐겨찾기