destroyCancellationToken でオブジェクトの寿命管理

<sup>更新履歴： シーンの生存期間を MonoBehaviour で管理する意味はなかった。</sup><br/>&nbsp;


以前 DI コンテナを使ってインスタンスのライフサイクルを管理すると良さそうという話を書いたんですが、

機械的にコンストラクターインジェクションを解決したい、という状況に中々巡り合えないまま気が付いたら `MonoBehaviour.destroyCancellationToken` というものが Unity に追加されてました。

https://qiita.com/sator_imaging/items/e2a4f71f2d385d70ff1b


- [全部 `CancellationToken` で良いじゃん](#全部-cancellationtoken-で良いじゃん)
- [ヘルパークラス `LifecycleBehaviour`](#ヘルパークラス-lifecyclebehaviour)
    - [つかいかた](#つかいかた)
    - [ソースコード](#ソースコード)
        - [API Reference](#api-reference)
    - [考慮したこと／すべきこと](#考慮したことすべきこと)
- [「アップデートマネージャー」機能](#アップデートマネージャー機能)
    - [特徴](#特徴)
    - [グローバルアップデートマネージャー的なモノ](#グローバルアップデートマネージャー的なモノ)
    - [TODO: `RunOnMainThread` もどうせなら欲しい](#todo-runonmainthread-もどうせなら欲しい)
    - [余談：アップデートマネージャーの必要性](#余談アップデートマネージャーの必要性)
- [おわりに](#おわりに)





# 全部 `CancellationToken` で良いじゃん

ということで、インスタンスの寿命管理は全部 `CancellationToken.Register(...)` で良いじゃんと思うようになりました。DI コンテナなんて要らなかったんだ。

せっかくなので Unity 2021 でも使えてアロケーションも回避するヘルパーも用意しちゃいましょう。（私が行ったテストに間違いがなければ）これがパフォーマンス的にもベストな選択です。

> 👇 Curried というテクニックは .NET 8 環境だとめっちゃ遅くなる。（コンソールアプリ + BenchmarkDotNet でテスト）
> 
> https://gist.github.com/ufcpp/b2e64d8e0165746effbd98b8aa955f7e
>
> Unity IL2CPP 環境だとそもそもこのテクニックは無意味（遅くもならず早くもならず）で、静的クラスの Action に静的メソッドを挿す（`static Action = StaticMethod;`）という方法だけが遅くなる。
> IL2CPP/.NET 共に一番早かったのは `static Action = () => { ... };`（静的関数じゃなくダイレクトでアクションを挿す）って方法。


<p><details lang="ja" --open><summary><code>Action</code> のアロケ回避の確認</summary>

実行環境： https://sharplab.io/ （`Results = C#`）

```csharp
using System;
using System.Threading;

public class C
{
    // token.Register(Action<object>, object) にこれを渡すとアロケ回避＆パフォーマンス的にもベスト
    readonly static Action<object?> Disposer = obj =>
    {
        if (obj is IDisposable disposable)
            disposable.Dispose();
    };
    
    public class Test : IDisposable
    {
        public void Dispose() {}
    }

    public void M()
    {
        CancellationToken token = new();
        
        // non alloc
        var test = new Test();
        var test2 = new Test();
        token.Register(Disposer, test);
        token.Register(Disposer, test2);

        // alloc
        var test3 = new Test();
        token.Register(test3.Dispose);
        token.Register(() => test3.Dispose());
    }
}
```


**結果**

`static Action = () => { ... }` は結構複雑な、、、暗黙的にクラスを定義してその静的フィールドにインスタンスを挿してさらにそのインスタンスのメソッドを呼んでる。

```csharp
public class C
{
    public class Test : IDisposable
    {
        public void Dispose()
        {
        }
    }

    [CompilerGenerated]
    private sealed class <>c__DisplayClass2_0
    {
        public Test test3;

        internal void <M>b__0()
        {
            test3.Dispose();
        }
    }

    [Serializable]
    [CompilerGenerated]
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();

        internal void <.cctor>b__4_0(object obj)
        {
            IDisposable disposable = obj as IDisposable;
            if (disposable != null)
            {
                disposable.Dispose();
            }
        }
    }

    [System.Runtime.CompilerServices.Nullable(new byte[] {
        0,
        2
    })]
    private static readonly Action<object> Disposer = new Action<object>(<>c.<>9.<.cctor>b__4_0);

    public void M()
    {
        <>c__DisplayClass2_0 <>c__DisplayClass2_ = new <>c__DisplayClass2_0();
        CancellationToken cancellationToken = default(CancellationToken);
        Test state = new Test();
        Test state2 = new Test();
        cancellationToken.Register(Disposer, state);
        cancellationToken.Register(Disposer, state2);
        <>c__DisplayClass2_.test3 = new Test();
        cancellationToken.Register(new Action(<>c__DisplayClass2_.test3.Dispose));
        cancellationToken.Register(new Action(<>c__DisplayClass2_.<M>b__0));
    }
}
```

<!------- End of Details JA Tag -------></details></p>





# ヘルパークラス `LifecycleBehaviour`

出来上がったクラスは Unity 2021 でも `DestroyWith` 拡張メソッドが使えるようになってて、Unity メインメニューの `Tests > LifecycleManager > ...` から簡易的なテスト（手作業）が実行可能。

> なんとなく `CancellationToken.Register` の戻り値を投げ捨てるのが憚られたので Unity エディター上では `WeakReference` で保持して状況が確認できるように。



## つかいかた

至って単純。

拡張メソッドの名前は `BindTo` も良いなと思ったけど Unity らしく `Destroy` を含む名前にした。

> `CancellationToken.Register` で登録したアクションの実行順は LIFO。最後にバインドしたオブジェクトから先に破棄される。（もちろんその前にオーナーが破棄される）

```csharp
using SatorImaging.LifecycleManager;

// Unity 2021 でも動く
gameObject.DestroyWith(cancellationToken);
disposable.DestroyWith(tokenOrMonoBehaviour);
unityObj.DestroyUnityObjectWith(tokenOrBehaviour);  // Unity オブジェクトは別メソッド（理由は後述

// Unity シーンの生存期間を取得してバインド
var sceneLifetime = SceneLifetime.Get(gameObject.scene);
disposable.DestroyWith(sceneLifetime);
sceneLifetime.Token.Register(() => DoSomethingOnSceneUnloading());  // シーンのアンロード時に何かする

// Unity シーンに紐づいたライフサイクル（MonoBehaviour）を取得
var sceneLC = SceneLifecycle.Get();

// 生存期間の連結
var root = LifecycleBehaviour.Create("Root Lifecycle", dontDestroyOnLoad: false);
var child = LifecycleBehaviour.Create("Child Lifecycle", dontDestroyOnLoad: false);
var grand = LifecycleBehaviour.Create("Grandchild Lifecycle", dontDestroyOnLoad: false);
child.gameObject.DestroyWith(root);
grand.gameObject.DestroyWith(child);
    // child/grand はバインド後にシーンの破棄に巻き込まれるのを防ぐため DontDestroyOnLoad される

// デバッグ他の用途で使えるアクション。null なら何もしない。
// デフォルトで関連付けを WeakReference として保存するアクションが挿さってる（Unity エディター上のみ）
LifetimeExtensions.DebuggerAction = (obj, token, ticket, ownerOrNull) =>
{
    // バインド状況をログりたいとかそういう用途
    Debug.Log($"Target Object: {obj}");
    if (ownerOrNull != null)
        Debug.Log($"Lifetime Owner: {ownerOrNull}");
    Debug.Log($"CancellationToken: {token}");
    Debug.Log($"CancellationTokenRegistration: {ticket}");
};
```


## ソースコード

最初は100行程度だったんだけど、テストとドキュメントを含むとは言え1000行になっちゃうとは。

https://github.com/sator-imaging/Unity-LifecycleManager/blob/active/v0.9/LifecycleBehaviour.cs


### API Reference

https://sator-imaging.github.io/Unity-LifecycleManager/api/SatorImaging.LifecycleManager.html




## 考慮したこと／すべきこと

:::note info

シーン間でインスタンスの生存期間のバインドをすると物事が複雑になりそうだから制限。ただ `monoBehaviour.destroyCancellationToken` にオブジェクトを直接バインドすればシーン間で関連付けることも可能。（というかトークンから発行元オブジェクトを取得する方法がないから防げないだけ）

シーンのアンロードによる破棄が起きると OnDestroy の実行順が代わっちゃうので、生存期間がバインドされた GameObject は自動的に `DontDestroyOnLoad` に移して実行順を保証。

でもコンポーネント（MonoBehaviour）は親を勝手に `DontDestroyOnLoad(component.gameObject)` しちゃうとトラブルになりそうだからそのまま。なのでシーンのアンロードとオーナーの破棄の両方の理由で OnDestroy が発生しうる。めちゃくちゃ厄介。

これは Unity がシーンのアンロード時にすべての MonoBehaviour の destroyCancellationToken をキャンセル、そのコールバックすべてが終わったのを確認してから GameObject の破棄（OnDestroy）を開始する、ってしないと対応できない。

でも `CancellationToken.Register` のコールバックの終了を検出する方法はないから多分そういう処理が Unity で行われる可能性はない。コンポーネント `MonoBehaviour` の寿命を `destroyCancellationToken` に紐づける場合はその辺り考慮する必要がある。基本はやめた方が良いと思う。

（最初に挿したコールバックが最後に実行されるから、全ての MonoBehaviour にコールバック終了を通知するコールバックを挿すとかいう無茶をすれば検知出来なくもない）

※ コンポーネント側はオーナーより先に破棄されて困るってことはなさそうだけどオーナー側は困りそう？ `destroyCancellationToken` で紐づけたからコンポーネントが `MissingReferenceException` になる筈はない、が保証されないので結局都度チェックする必要が出てくる。

:::

当然かもだけど `destroyCancellationToken` は非同期処理を MonoBehaviour 内で実行するときに渡すことしか考えて無さそうな？？ `destroyCancellationToken.Register` で Unity エンジン絡みの重要な処理を登録しちゃうのは少し怖いね。





# 「アップデートマネージャー」機能

勢いに任せて以下に対処する、所謂アップデートマネージャー機能も付けた。

https://blog.unity.com/ja/engine-platform/10000-update-calls





## 特徴

各 Update は `Early` `無印` `Late` の３ステージに分かれていて実行順を保証、ただし各ステージに登録されたアクションの実行順は登録／解除の最適化の為に**保証しない**という実装。

> `MonoBehaviour` も保証してないからダイジョブ。

ライブラリ内のコンポーネントの実行順制御はこの３ステージを使っとけば後々面倒が無くなる。

```csharp
var lifecycle = LifecycleBehaviour.Create("My Lifecycle", false);

// 登録
lifecycle.RegisterUpdateEarly(...);
lifecycle.RegisterUpdate(...);
lifecycle.RegisterUpdateLate(...);

lifecycle.RegisterFixedUpdateEarly(...);
lifecycle.RegisterFixedUpdate(...);
lifecycle.RegisterFixedUpdateLate(...);

lifecycle.RegisterLateUpdateEarly(...);
lifecycle.RegisterLateUpdate(...);
lifecycle.RegisterLateUpdateLate(...);

// 削除　※ Action のインスタンスが完全に一致しないと削除できないので削除するつもりなら参照を取っておく
var entry = lifecycle.RegisterUpdateEarly(...);
lifecycle.RemoveUpdateEarly(entry);
```

最初と最後にアプリ／システム用途のステージとして `Initialize` `Finalize` もある。（合計５ステージ）

```csharp
lifecycle.RegisterUpdateInitialize(...);
// --> Early -> 無印 -> Late が続いて
lifecycle.RegisterUpdateFinalize(...);
```

ちょっとステージが多すぎる気もするけど、登録しない限りは内部に配列も確保しないしループごとにヌルチェックするだけだからそこまでコストはかかってないハズ。性質上インスタンスの生存期間も長いだろうしたったの15個、問題ない。





## グローバルアップデートマネージャー的なモノ

アップデートマネージャーより短命なインスタンスとそれに依存したアクションを登録するケース。

```csharp
var globalLC = LifecycleBehaviour.Create("Global Lifecycle", true);  // アプリ起動中ずっと存命

instance.DestroyWith(token);
globalLC.RegisterUpdate(() => instance.NoErrorUntilDispose(), token);  // トークンで登録解除する

// 同じトークンを渡して登録解除されるようにしておかないと、依存しているインスタンスが破棄されてしまいエラーに
globalLC.RegisterUpdate(() => instance.NoErrorUntilDispose());
```





## TODO: `RunOnMainThread` もどうせなら欲しい

https://qiita.com/odayushin/items/fd729c2752121c84e0e6

Google API にあるやつ。なんか長い。

https://github.com/googlesamples/unity-jar-resolver/blob/master/source/VersionHandlerImpl/src/RunOnMainThread.cs

Firebase API のやつ。さらに長い。

- https://github.com/firebase/firebase-unity-sdk/blob/main/app/platform/Dispatcher.cs#L23
- https://github.com/firebase/firebase-unity-sdk/blob/main/app/platform/Mono/FirebaseHandler.cs#L67





## 余談：アップデートマネージャーの必要性

稀に（でもない？） 外部向けなのに `Update` `LateUpdate` でコンポーネントの実行順を制御しちゃってる Unity ライブラリなんかがあったりするけど、アプリケーションレベルの実行順制御機構をライブラリで使ってしまうと大抵面倒なことになる。

> ライブラリ単位では当然問題ないが、いざアプリに組み込もうとすると色々とつじつまが合わなくなる。
> 
> Unity のライフサイクルの数が足りない、カスタマイズしたい、あれが `Update` だから `LateUpdate` 使うしかない、`UnityEngine.LowLevel.PlayerLoop` でゴリゴリにカスタマイズしよう、って問題の根本的な原因は安易な `Update` `LateUpdate` の利用による実行順制御。

単純／決め打ちで構わないのでアップデートマネージャーを作っておくと、各アップデートマネージャーの実行順を制御するだけになって大分楽になるのでおススメ。

変に `MonoBehaviour` を嫌っているのに実行順の制御は MonoBehaviour のライフサイクルに強く依存してる、とかいうライブラリみるとマジで、、、って感じですよね。気を付けたいですね。

:::note info

MonoBehaviour を作ったら必ず自分のライフサイクルを使わなきゃいけない訳じゃない。Unity の API の中には `Gizmos` みたいに特定のライフサイクルでしか実行できないものもあるけど、MonoBehaviour 自身のライフサイクルで実行しなきゃならないって制限を課されてる API は存在しない。

なので事情があって一つの処理を複数の MonoBehaviuor に分けざるを得ないとしても、本当は一緒に処理したかった MonoBehaviour 群の中心となるコンポーネントの `Update` で他のコンポーネントの更新処理を順序立てて呼び出してやれば本来は済む話。単純な処理ならアップデートマネージャーなんて大袈裟なモノを作る必要もない（中心となるコンポーネントが結果的にその役割を担ってるけど）

<!--
そういうあっちがこっちにあーだこーだはキレイじゃない！ って `Update` `LateUpdate` を使い始めると信じられないぐらい惨事になるから絶対にやめた方が良い。複雑なモノを複雑じゃなくする方法が必ずあるとは限らない。複雑なんだから。探せば作ったものに一致するデザインパターンが絶対見つかるはずだからそれを使ったんだよねーで納得しておく。それでもどうしても我慢できないなら MonoBehaviour への依存を完全排除するとかとことんまでやりきる。
-->

その辺さえ押さえとけば特定のデバイス／ネットワークからの入力処理だけはどうしても早めに実行したい！ っていうよくある状況を `ScriptExecutionOrder` で設定する程度で十分になる。

:::

```csharp
// 同じライブラリ内の実行順制御に Update LateUpdate 使われちゃうとこういう単純なことも出来なくなる
// メソッドを公開してくれてればなんとか出来るけど、デフォルトのまま非公開のケースが多いし対象が多かったら終わる
class GeneralManagerOfUpdateManagers : MonoBehaviour
{
    public LifecycleBehaviour lifecycle1;
    public LifecycleBehaviour lifecycle2;
    public LifecycleBehaviour lifecycle3;

    void OnEnable()
    {
        lifecycle1.enabled = false;
        lifecycle2.enabled = false;
        lifecycle3.enabled = false;
    }
    void Update()
    {
        lifecycle3.Update();  // 各ライブラリ内のステージ実行順は維持しながらアプリ全体の順序を制御
        ○○.Update();
        △△.LateUpdate();     // 単に○○の後に実行したいから Late なだけで本来 Update で実行すべきもの
        lifecycle1.Update();  // そもそもライブラリが自分の抱えてるコンポーネントの実行順を適切に制御
        lifecycle2.Update();  // してればこういう余計なつじつま合わせは要らない
    }
    void LateUpdate()
    {
        lifecycle1.LateUpdate();
        lifecycle3.LateUpdate();
        lifecycle2.LateUpdate();
    }
    void FixedUpdate()
    {
        lifecycle2.FixedUpdate();
        lifecycle3.FixedUpdate();
        lifecycle1.FixedUpdate();
    }
}
```




# おわりに

特別なことしなくても Unity のシーンとかコンポーネントにピュア C# クラスの寿命を紐づけられるようになる `destroyCancellationToken` 便利。

勢いで作り切ったからアプリに組み込んで使うのはこれからだけど Unity コンポーネントの寿命の紐づけを禁止して `GameObject` を必ず `DontDestroyOnLoad` するようにすれば信頼性は上がりそう。

アップデートマネージャーは MonoBehaviour が最低千個はないと意味が出てこないからインベントリのアイテム一覧とかかなー 使うなら。


--


以上です。お疲れ様でした。