随风逐叶 随风逐叶
首页
  • Quick Reference (opens new window)
  • EgretEngine开发者文档 (opens new window)
  • TinaX框架
  • SSH教程
  • VSCode插件开发
关于
  • 分类
  • 标签
  • 归档

rontian

从事游戏开发10多年的老菜鸟一枚!
首页
  • Quick Reference (opens new window)
  • EgretEngine开发者文档 (opens new window)
  • TinaX框架
  • SSH教程
  • VSCode插件开发
关于
  • 分类
  • 标签
  • 归档
  • 框架简介
  • TinaX.Core
  • 基于TinaX创建一个扩展库
  • TinaX.VFS
  • TinaX.UIKit
  • TinaX.I18N
  • TinaX.Lua
  • XLua

  • Google.Protobuf
  • Lua-Protobuf
  • 一些优秀的第三方库

    • CatLib

    • UniRx

      • 简介
      • 基础教程
      • 官方入门文档
      • UniRx入门系列一
      • UniRx入门系列二
        • UniRx入门系列三
        • UniRx入门系列四
        • UniRx入门系列五
      • UniTask

    目录

    UniRx入门系列二

    # UniRx入门系列二

    # 上节回顾

    在上一节中,解释了IObsrver的接口定义如下:

    using System;
    namespace UniRx
    {
        public interface IObserver<T>
        {
            void OnCompleted();
            void OnError(Exception error);
            void OnNext(T value);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    上一节中,只讲述了OnNext,下面我们将讨论一下在流执行过程中的OnError、OnCompleted和Dispose。

    # UniRX中的OnNext、OnError、OnCompleted

    在UniRX中的对任意流的操作,最终都会转化为这三类中的任何一类,其具体用途如下:

    • OnNext 在事件发生时,发出通知,观察者会执行对应的订阅操作
    • OnError 处理流的过程中发出异常时发出通知
    • OnCompleted 当前流结束时,发出通知

    OnNext是UniRX中最常用的操作,通常代表事件通知;即,当事件发生时,发出事件已经发出的通知

    # 例子1: 整数通知(发布)

    var subject = new Subject<int>();
    
    subject.Subscribe(x => Debug.Log(x));
    
    subject.OnNext(1);
    subject.OnNext(2);
    subject.OnNext(3);
    subject.OnCompleted();
    
    1
    2
    3
    4
    5
    6
    7
    8

    输出如下:

    1
    2
    3
    
    1
    2
    3

    本例中,只是一个简单的整数通知,然后在订阅端通过Debug.Log()打印。

    # 例子2:一个没有意义的值的通知(发布)

    var subject = new Subject<Unit> ();
    		subject.Subscribe (
    			onNext:x => Debug.Log (x),
    			onCompleted:()=>{Debug.Log("OnComplete");});
    subject.OnNext(Unit.Default);
    
    1
    2
    3
    4
    5

    输出结果如下:

    ()
    
    1

    例子二中使用了一个Unit类型的特殊类型,这种类型表示当前信息内容是没有意义的。这对于事件的发布时机来说是很重要的,OnNext()中的内容在任何情况下都可以使用。比如,例子三中,可以利用在场景初始化完成时或者Player玩家死亡时。

    # 例子三:以Unit作为传递值以通知(发布)场景初始化完成

    public class UniRxUint : MonoBehaviour {
        private Subject<Unit> initialedSubject = new Subject<Unit> ();
    	public IObservable<Unit> OnInitializedAsync => initialedSubject;
    
    	void Start () {
    		StartCoroutine(GameInitialitializeCoroutine());
    		OnInitializedAsync.Subscribe(_=>{
    			Debug.Log("场景初始化完成");
    		});
    	}
    	IEnumerator GameInitialitializeCoroutine () {
    		/*
    		一些耗时的初始化处理,自行脑补
    		 */
    		yield return null;
    		initialedSubject.OnNext (Unit.Default);
    		initialedSubject.OnCompleted ();
    	}
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19

    这种情况下,我们只需要发出场景初始化完成的通知,并不需要发布值,便可使用Unit来表示。

    # OnError

    OnError,如名字一样,当在处理流的过程中发生异常时发出异常的通知。OnError可以在流中进行Catch处理(异常捕获),或者直接到达Subscribe方法,再进行处理。如果OnErro消息到达Subscribe,那么,流的订阅将会被终止并且销毁。

    # 例子4:在Subscribe接收过程中发生错误

        var stringSubject = new Subject<string> ();
    		stringSubject
    			.Select (str => int.Parse (str))
    			.Subscribe (
    				onNext: v => { Debug.Log ("转换成功:" + v); },
    				onError : ex => { Debug.Log ("转换失败: " + ex); }
    			);
    	stringSubject.OnNext ("1");
    	stringSubject.OnNext ("2");
    	stringSubject.OnNext ("100");
    	stringSubject.OnNext ("Hello");
    	stringSubject.OnCompleted ();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

    输出结果如下:

    成功:1
    成功:2
    成功:100
    转换失败:System.FormatException: Input string was not in a correct format.
    
    1
    2
    3
    4

    在示例4中,OnNext发出的字符串被Select(选择或者转换)操作符解析并打印出Int类型的流;通过OnError,就可以在处理流的过程中,发生异常时,便可知道得到异常的细节。如果流收到异常之后没有被处理,那么当前流就会被终止。

    # 例子5:处理流过程中发生异常,则重新订阅(发布)

    var stringSubject = new Subject<string> ();
    		stringSubject
    			.Select (str => int.Parse (str))
    			.OnErrorRetry ((FormatException ex) => {
    				Debug.Log ("本次转换失败 :" + ex);
    			})
    			.Subscribe (
    				onNext: v => { Debug.Log ("转换成功:" + v); },
    				onError : ex => { Debug.Log ("转化失败: " + ex); }
    			);
    		stringSubject.OnNext ("1");
    		stringSubject.OnNext ("2");
    		stringSubject.OnNext ("100");
    		stringSubject.OnNext ("Hello");
    		stringSubject.OnNext ("250");
    		stringSubject.OnNext ("300");
    		stringSubject.OnNext ("550");
    		stringSubject.OnCompleted ();
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    在示例5中,在处理流的过程中,出现了异常,使用OnErrorRetry对流进行重建并继续订阅。当前流并没有被终止,而是继续想传递。OnErrorRetry是一个异常处理操作符,当OnError是一个特定的异常时,当前流从Subscribe重新开始,即Subject重新注册IObserver.

    # 一些可用的异常处理操作符

    • Retry 当OnError被触发时,Retry会再次重试。
    • Catch 捕获错误,进行错误处理,将其替换为另外一个流
    • CatchIgnore 捕获错误并处理,将OnErroe转换为OnCompleted
    • 捕获错误处理后,重新Subscribe

    # OnCompleted

    OnCompleted 当流完成时发出通知,并且之后不再发出通知。如果OnCompleted消息到达Subscribe,和OnErrod一样,该流的订阅将会被终止和销毁。因此,可以向流发出OnCompleted来终止流的订阅,同样,也可以用此方法来清理流。

    # 例子6:检测OnCompleted

     Subject<string> stringSubject = new Subject<string>();
    
            stringSubject.Subscribe(
                onNext: x => Debug.Log(x),
                onCompleted: () =>
                {
                    Debug.Log("OnCompleted");
                });
    
    1
    2
    3
    4
    5
    6
    7
    8

    输出如下:

    1
    2
    OnCompleted
    
    1
    2
    3

    在Subscribe的重载方法中定义OnNext、OnCompleted

    # Subscribe的重载方法

    我们之前介绍的Subscribe实际上有多个重载方法,你可以根据你的事件流选着满足你要求的重载方法,如下:

    • Subscribe(IObserver observer) 最基本的订阅,参数代表观察者对象
    • Subscribe() 不对信息做任何处理
    • Subscribe(Action onNext, Action onError) 传递流,并处理异常
    • Subscribe(Action onNext, Action onCompleted)传递流,流被终止时发布消息
    • Subscribe(Action onNext, Action onError, Action onCompleted)传递流,并处理操作流过程中的各种信息

    # 终止流,结束订阅

    接下来,我们解释一下IObservable “IDisposable”

    public interface IObservable<T>
    {
        IDisposable Subscribe(IObserver<T> observer);
    }
    
    1
    2
    3
    4

    IDisposable是C#中的一个接口,有一个"Dispose"方法,用于对资源的释放。

    namespace System
    {
        public interface IDisposable
        {
            void Dispose();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7

    如果Subscribe的返回值是IDisposable,那么就可以终止流的订阅,并释放流。

    # 例子7:使用Dispose结束流的订阅

    void Start()
        {
            var subject = new Subject<int>();
            var disposable = subject.Subscribe(x => Debug.Log(x), () => Debug.Log("OnCompleted"));
    
            subject.OnNext(1);
            subject.OnNext(2);
    
            disposable.Dispose();
    
            subject.OnNext(100);
    
            subject.OnNext(10);
    
            subject.OnCompleted();
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    输出如下:

    1
    2
    
    1
    2

    如上,可以通过调用Dispose来终止订阅。这里需要注意一点,如果使用Dispose来终止流的订阅,那么OnCompleted将不会被出发。所以,如果你在OnCompleted中写了停止流时的一些触发处理,那么使用Dispose释放流之后,是不会运行的。

    # 例子8:只终止(释放)特定的流

    void Start(){
       var subject = new Subject<int>();
            var disposable1 = subject.Subscribe(
                onNext: x => Debug.Log("Disposable 1:" + x),
                onCompleted: () => Debug.Log("OnCompleted: 1"));
            var disposable2 = subject.Subscribe(
                onNext: x => Debug.Log("Diaposable 2:" + x),
                onCompleted: () => Debug.Log("OnCompleted: 2"));
    
            subject.OnNext(1);
            subject.OnNext(2);
            //释放第一个流
            disposable1.Dispose();
            //第二个流未被释放,继续传递
            subject.OnNext(3);
            subject.OnCompleted();
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    # 流的生命周期和Subscribe终止时间

    在使用UniRX的过程中,时刻注意流的生命周期是非常必要的。频繁创建和删除对象会导致应用性能下降。

    # 是什么控制着流的流动和传递

    在对流进行生命周期管理时,你需要意识到,是什么在控制着流的传递,是什么控制着流。事实上,流的实体是Subject,如果这个Subject被销毁,那么,当前流也会被销毁和终止。之前说过,Subscribe是指在Subject上注册的响应订阅的处理函数。也就是说,在在Subject的内部保留着调用函数的列表(以及与该函数相连的方法链)。这也说明了,Subject是流的管理对象。一旦Subject被全部销毁或者终止,那么流也会被销毁和终止。反过来说,只要Subject继续存在,流就还会继续运转。如果你在流开始传递前丢弃流中需要引用的对象,那么流可能会继续往下传递,从而导致应用性能下降,引起内存泄漏,或者应用直接抛出空异常。所以,在使用流的时候,需要特别细心,一定养成不使用的流,及时Dispose或者OnCompleted;

    # 例子9:使用UniRX中的事件通知重写Player的坐标

    假设有一个动作游戏,游戏思路如下:

    • 有一个Player可以操控
    • 给定一个计时器(倒计时)
    • 当倒计时结束时,将Player的坐标重置为起点坐标
    • Player如果超出给定范围,就会被杀死

    倒计时:

    public class TimeCounterUniRX : MonoBehaviour
    {
        private Subject<int> timerSubject = new Subject<int>();
        public IObservable<int> OnTimeChanged => timerSubject;
        void Start()
        {
            StartCoroutine(TimerCoroutine());
            timerSubject.Subscribe(x => Debug.Log(x));
        }
        IEnumerator TimerCoroutine()
        {
            var time = 10;
            while (time >= 0)
            {
                timerSubject.OnNext(time--);
                yield return new WaitForSeconds(1);
            }
            timerSubject.OnCompleted();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20

    Player玩家:

    public class Player : MonoBehaviour
    {
        public TimeCounterUniRX timeCounterUniRX;
        public float moveSpeed = 10.0f;
    
        void Start()
        {
            timeCounterUniRX.OnTimeChanged
             .Where(x => x == 0)
             .Subscribe(_ =>
             {
                 transform.localPosition = Vector3.zero;
             });
        }
        void Update()
        {
            var xzValue = new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical"));
            if (xzValue.magnitude > 0.1f)
            {
                transform.localPosition += xzValue * moveSpeed * Time.deltaTime;
            }
            if (transform.localPosition.x > 10)
            {
                Debug.Log("Game Over");
                Destroy(this.gameObject);
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28

    当计时器到达0时,如果,Player未被销毁。Player坐标正确的覆盖到初始位置。如果在计时器未到达0时,Player被销毁(使其position.x在10秒内>10);当计时器器到达0时,会抛出:MissingReferenceException: The object of type ‘Player’ has been destroyed but you are still trying to access it.,也就是说,当计时器到达0时,我们才发现,Player已经不存在了。

    # 原因何在呢?

    看下面这段代码:

     void Start()
        {
            timeCounterUniRX.OnTimeChanged
             .Where(x => x == 0)
             .Subscribe(_ =>
             {
                 transform.localPosition = Vector3.zero;
             });
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    正如我们之前说过的那样,流的管理者是Subject,由Subject维持着流的传递和运转,就算Player被Destroy销毁,流依然由TimeCounterUniRX中的Subject维持,所以流依然继续保持,当流满足限定条件时,会访问订阅者的对象,但是,订阅者的对象已经被销毁了,所以才会引发空引用异常。结论就是,如果流的生命周期和对象的生命周期不一致,就会导致对象行为出现异常。

    # 如何处理这种情况

    处理的方法很简单,那就是当Player对象被销毁时,终止订阅流程就可以了。UniRX提供了多种终止并释放流的方式,下面的例子展示一个最简单的AddTo的使用方式。

    void Start()
        {
            timeCounterUniRX.OnTimeChanged
             .Where(x => x == 0)
             .Subscribe(_ =>
             {
                 transform.localPosition = Vector3.zero;
             }).AddTo(gameObject);
        }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    使用了AddTo方法指定当前流的生命周期和this.gameobject的生命周期一致,这样一来,便不会出现之前销毁Player对象之后,任然抛出MissReferenceException的问题了。即当Player被销毁之后,当前流的订阅也会被停止。

    # 总结

    在流的执行过程中,有三种类型的信息传递:

    • OnNext 在事件触发时发出通知消息
    • OnError 在处理流的过程中抛出异常
    • OnCompleted 流结束时发布消息(通知)

    停止订阅流的方式:

    • Subscribe的OnCompleted,检测OnCompleted是否被触发
    • Subscribe的OnError,执行流过程中,触发异常
    • Subscribe返回的IDisposable 的Dispose方法

    流的生命周期和对象生命周期之间的关系:

    • 流的管理者是Subject,流的运转依赖于Subject
    • 使用流的过程中,养成自动Dispose或者OnCompleted的习惯去释放或者终止流,否者会造成内存泄漏或者抛出其它异常信息。
    上次更新: 2023/10/17, 17:20:06 访问次数: 0
    UniRx入门系列一
    UniRx入门系列三

    ← UniRx入门系列一 UniRx入门系列三→

    最近更新
    01
    一些Shell常用的功能写法整理
    10-20
    02
    删除git仓库submodule的步骤
    10-20
    03
    django基本命令
    10-16
    更多文章>
    Copyright © 2017-2025 随风逐叶
    沪ICP备18008791号-1 | 沪公网安备31011502401077号

    网站访问总次数: 0次
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式