はじめに
以下の環境で実行しています
- 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上でエラーが出るようになります
こうしてエラーが出てくれれば忘れることもない(実行できないので)
導入手順
++++++++++++++++++++++++++++++++++++++++++++++++
Unity2021.2.0b2 で動作を確認しています。
Unity2020.3.X系だとUnityでエラーが出ませんでした...(VisualStudioではエラーが出る)
バグか自分の設定が悪いのか.. 調査中
++++++++++++++++++++++++++++++++++++++++++++++++
1. MessagePipe.Analyzer.dll を Unityに入れる
MessagePipeのGithubにMessagePipe.Analyzer.dll が上がっています。
(以下GithubのReleaseを参照)
2. 設定変更
MessagePipe.AnalyzerのInspectorで以下のように設定変更すること
-
SelectPlatforms for Plugin
のチェックをすべて外す - 右下のAssetLabelsボタンをクリックして
RoslynAnalyzer
を入力してEnterを押してラベルを付ける
ロード終了後、Subscribeを処理していない箇所に対してUnityがエラーを出すようになります
3. Visualstudioでもエラーを出す
VisualStudio上でもエラーを出すためには、Unityに対して更に一工夫する必要がありました。
同じくCysharpが公開しているCsprojModifierを導入します
公開されている最新バージョンのパッケージをインストールします
Unity > Project Settings > C# Project Modifier
を開き、Add Roslyn Analyzer references to .csproj
にチェックを付け、 Regenerate project files を押して .csproj を作り直します。
終了後再度VIsualStudioを開き直したらVisualStudio上でも検知できるようになりました
(現状、Unity2020.2以降のバージョンを使用している場合VisualStudioCode, Rider ではこの機能は使用できないようです。
MacのVisualStudioでは正常に確認ができました)
Unityでエラーが出る以上実行ができないのでUnityでカバーしていればまあよし!とも捉えられる
MessagePipeDiagnostics を使用する
MessagePipeは更に機能があり(凄い)、実行中のプロジェクトで現在Subscribeされている数や情報を取得できる機能があります。(MessagePipeDiagnosticsInfo)
そして気軽に情報を見れるようにSubscribeしている箇所のモニターが出来る拡張機能が用意してあります(凄い)
Window > MessagePipeDiagnostics
を開くと、EditorWindowが立ち上がります。
そして実行中にWindowを見みると以下画像のようにSubscribeしている箇所が確認できます
この機能を利用するには GlobalMessagePipe.SetProvider
を予め設定しておく事と、optionのEnableCaptureStackTrace
をtrueにしておく必要があります。
終わりに
今回は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 = "テストメッセージ" });
}
}
}
}