LoginSignup
7
3

More than 1 year has passed since last update.

【Unity】MessagePipeのSubscribe, Dispose処理について

Last updated at Posted at 2021-07-04

はじめに

以下の環境で実行しています
- MacOS
- Unity2021.2.0b2
- MessagePipe v1.6.1
- Zenject

前回のMessagePipeの記事

こちらのMessagePipeのReadmeを元に学んだことを記事にまとめています

Subscribe, Dispose

MessagePipeはSubcribeでイベント受信(購読)を行います。

public class Hoge
{
    public Hoge()
    {
        // MyEventの通知を受け取る
        var subscriber = MessagePipe.GlobalMessagePipe.GetSubscriber<MyEvent>();
        subscriber.Subscribe(ev =>
        {
            // 購読処理
        });
    }
}

Subscribeの戻り値はIDisposable
このIDisposableを適切に処理しなければリークが発生します。

つまり、クラスが破棄されたとしてもイベント購読し続けることになる
(バグが出る可能性が非常に高い!)

基本的には購読しているクラスの破棄と同時にイベントの購読も消したいはず。
今回はイベントの破棄を担当するDisposeについて書いていきます。

非Monobehaviourクラスでイベントを破棄したい場合

public class Hoge
{
    private readonly IDisposable _disposable;

    public Hoge()
    {
        var bag = DisposableBag.CreateBuilder();

        // MyEventの通知を受け取る
        {
            var subscriber = MessagePipe.GlobalMessagePipe.GetSubscriber<MyEvent>();
            subscriber.Subscribe(ev =>
            {
                // 購読処理
            }).AddTo(bag);
        }

        // stringの通知を受け取る
        {
            var subscriber = MessagePipe.GlobalMessagePipe.GetSubscriber<string>();
            subscriber.Subscribe(ev =>
            {
                // 購読処理
            }).AddTo(bag);
        }

        _disposable = bag.Build();
    }

    public void Close()
    {
        // _disposable.Disposeですべて破棄される。
        // 必ずクラス終了前に呼び出すこと
        _disposable.Dispose();
    }
}

Subscribeの戻り値に対してAddToメソッドを呼び出す

そこに DisposableBag.CreateBuilder() で作り出した DisposableBagBuilder を渡します。
そして _disposable.Dispose を呼び出すことでイベントすべての購読が破棄されます。

イベント単体の破棄であればSubscribeの戻り値をそのままメンバ変数に持ち、最後にDisposeするだけでも良

Disposeは明示的に呼び出す必要があるので注意

Monobehaviourを継承しているクラスでイベントを破棄したい場合

UniRxを組み込んでいる場合、Monobehaviourクラスはもう少し楽に破棄の処理ができます

namespace UniRx; // UniRx必須

public class Hoge : Monobehaviour
{
    void Awake()
    {
        // MyEventの通知を受け取る
        var subscriber = MessagePipe.GlobalMessagePipe.GetSubscriber<MyEvent>();
        subscriber.Subscribe(ev =>
        {
        }).AddTo(this); // 自分を渡してGameObjectの破棄時に削除をしてもらう
    }
}

UniRxではおなじみの処理ですが
AddToに自分自身を渡してGameObjectのライフサイクルに結びつけます。
これでGameObjectが破棄されるときにイベントも破棄されます。

Subscribeを適切にハンドリングしてない場合エラーを出す

Disposeの仕方はわかりましたが『Subcribeを適切に処理しているかどうか』は結局コードを書く人に委ねられている状態です。

忘れないように心がけていても抜けは必ず発生します(必ず)

しかし! (Unity2020.2以降であれば)Subcribeの戻り値をハンドリングしてないものがあれば、ビルド時にエラーになるMessagePipe.Analyzerという便利機能が公開されているようです

MessagePipe.AnalyzerはRoslyn analyzers という、ユーザー独自のコード解析を組み込める仕組みを利用されています(以下記事参考)


結果からいうと MessagePipe.Analyzer.dll を組み込むことで
Subscribeに対して何も処理をしていない場合Unityのコンソール上やVisualStudio上でエラーが出るようになります

  • VisualStudio
    ↓ Subscribeに対して何もしていない
    20210704173534.png

  • Unity
    20210704173442.png

こうしてエラーが出てくれれば忘れることもない(実行できないので)

導入手順

++++++++++++++++++++++++++++++++++++++++++++++++
Unity2021.2.0b2 で動作を確認しています。
Unity2020.3.X系だとUnityでエラーが出ませんでした...(VisualStudioではエラーが出る)

バグか自分の設定が悪いのか.. 調査中
++++++++++++++++++++++++++++++++++++++++++++++++

1. MessagePipe.Analyzer.dll を Unityに入れる

MessagePipeのGithubにMessagePipe.Analyzer.dll が上がっています。
(以下GithubのReleaseを参照)

DLLをダウンロード
20210630233610.png

そしてAsset以下のどこか適切なディレクトリにぶっこむ。
20210704174052.png

2. 設定変更

MessagePipe.AnalyzerのInspectorで以下のように設定変更すること

  • SelectPlatforms for Plugin のチェックをすべて外す
  • 右下のAssetLabelsボタンをクリックして RoslynAnalyzer を入力してEnterを押してラベルを付ける

20210704174456.png
ロード終了後、Subscribeを処理していない箇所に対してUnityがエラーを出すようになります

3. Visualstudioでもエラーを出す

VisualStudio上でもエラーを出すためには、Unityに対して更に一工夫する必要がありました。

同じくCysharpが公開しているCsprojModifierを導入します

公開されている最新バージョンのパッケージをインストールします

Unity > Project Settings > C# Project Modifier を開き、Add Roslyn Analyzer references to .csproj にチェックを付け、 Regenerate project files を押して .csproj を作り直します。

20210704175014.png

終了後再度VIsualStudioを開き直したらVisualStudio上でも検知できるようになりました

(現状、Unity2020.2以降のバージョンを使用している場合VisualStudioCode, Rider ではこの機能は使用できないようです。
MacのVisualStudioでは正常に確認ができました)
Unityでエラーが出る以上実行ができないのでUnityでカバーしていればまあよし!とも捉えられる

MessagePipeDiagnostics を使用する

MessagePipeは更に機能があり(凄い)、実行中のプロジェクトで現在Subscribeされている数や情報を取得できる機能があります。(MessagePipeDiagnosticsInfo)

そして気軽に情報を見れるようにSubscribeしている箇所のモニターが出来る拡張機能が用意してあります(凄い)

Window > MessagePipeDiagnostics を開くと、EditorWindowが立ち上がります。

20210701100112.png

そして実行中にWindowを見みると以下画像のようにSubscribeしている箇所が確認できます

20210704181756.png

この機能を利用するには GlobalMessagePipe.SetProvider を予め設定しておく事と、optionのEnableCaptureStackTraceをtrueにしておく必要があります。

20210704181941.png

終わりに

今回はSubscribeした結果のイベント購読破棄について書きました。
Subscribeは適切に処理をしないと不具合の原因になるので標準にAnalyer機能、モニターEditor機能が用意されているのは本当に素晴らしいと感じました。

今回のDispose周りについて更に理解したことができたら追記していきます

お試しコード

Disposeで破棄
using NUnit.Framework;
using MessagePipe;
using Zenject;
using UnityEngine;
using System;

namespace XXXX
{
    public class DisposableTest
    {
        // イベント送る方
        public class Publisher
        {
            [Inject] private IPublisher<MyEvent> _publisher;

            public void Send(MyEvent ev) =>
                _publisher.Publish(ev);
        }

        // イベント受け取る方
        public class Subscriber : IDisposable
        {
            private readonly IDisposable _disposable;

            public Subscriber(ISubscriber<MyEvent> subscriber)
            {
                var bag = DisposableBag.CreateBuilder();

                subscriber
                    .Subscribe(x => Debug.Log($"{x.Message}"))
                    .AddTo(bag);

                _disposable = bag.Build();
            }

            void IDisposable.Dispose()
            {
                _disposable.Dispose();
            }
        }

        // 送るイベント
        public class MyEvent 
        { 
            public string Message; 
        }


        private DiContainer _container;

        /// <summary>
        /// SubscribeのIDosposableをかならず何らかの形でハンドリングする必要がある。
        /// しなければリークする
        /// </summary>
        [Test]
        public void イベントを破棄する()
        {
            _container = new DiContainer();

            var option = _container.BindMessagePipe();
            _container.BindMessageBroker<MyEvent>(option);

            // イベントを受ける方
            using (_container.Instantiate<Subscriber>())
            {
                // イベントを投げる方
                var publisher = _container.Instantiate<Publisher>();
                publisher.Send(new MyEvent { Message = "テストメッセージ" });
            }
        }
    }
}

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3