在上一节中,解释了IObsrver的接口定义如下:
using System;
namespace UniRx
{
public interface IObserver<T>
{
void OnCompleted();
void OnError(Exception error);
void OnNext(T value);
}
}
上一节中,只讲述了OnNext,下面我们将讨论一下在流执行过程中的OnError、OnCompleted和Dispose。
在UniRX中的对任意流的操作,最终都会转化为这三类中的任何一类,其具体用途如下:
OnNext是UniRX中最常用的操作,通常代表事件通知;即,当事件发生时,发出事件已经发出的通知
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
本例中,只是一个简单的整数通知,然后在订阅端通过Debug.Log()打印。
var subject = new Subject<Unit> ();
subject.Subscribe (
onNext:x => Debug.Log (x),
onCompleted:()=>{Debug.Log("OnComplete");});
subject.OnNext(Unit.Default);
输出结果如下:
()
例子二中使用了一个Unit类型的特殊类型,这种类型表示当前信息内容是没有意义的。这对于事件的发布时机来说是很重要的,OnNext()中的内容在任何情况下都可以使用。比如,例子三中,可以利用在场景初始化完成时或者Player玩家死亡时。
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 ();
}
}
这种情况下,我们只需要发出场景初始化完成的通知,并不需要发布值,便可使用Unit来表示。
OnError,如名字一样,当在处理流的过程中发生异常时发出异常的通知。OnError可以在流中进行Catch处理(异常捕获),或者直接到达Subscribe方法,再进行处理。如果OnErro消息到达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
成功:100
转换失败:System.FormatException: Input string was not in a correct format.
在示例4中,OnNext发出的字符串被Select(选择或者转换)操作符解析并打印出Int类型的流;通过OnError,就可以在处理流的过程中,发生异常时,便可知道得到异常的细节。如果流收到异常之后没有被处理,那么当前流就会被终止。
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 ();
在示例5中,在处理流的过程中,出现了异常,使用OnErrorRetry对流进行重建并继续订阅。当前流并没有被终止,而是继续想传递。OnErrorRetry是一个异常处理操作符,当OnError是一个特定的异常时,当前流从Subscribe重新开始,即Subject重新注册IObserver.
OnCompleted 当流完成时发出通知,并且之后不再发出通知。如果OnCompleted消息到达Subscribe,和OnErrod一样,该流的订阅将会被终止和销毁。因此,可以向流发出OnCompleted来终止流的订阅,同样,也可以用此方法来清理流。
Subject<string> stringSubject = new Subject<string>();
stringSubject.Subscribe(
onNext: x => Debug.Log(x),
onCompleted: () =>
{
Debug.Log("OnCompleted");
});
输出如下:
1
2
OnCompleted
在Subscribe的重载方法中定义OnNext、OnCompleted
我们之前介绍的Subscribe实际上有多个重载方法,你可以根据你的事件流选着满足你要求的重载方法,如下:
接下来,我们解释一下IObservable “IDisposable”
public interface IObservable<T>
{
IDisposable Subscribe(IObserver<T> observer);
}
IDisposable是C#中的一个接口,有一个"Dispose"方法,用于对资源的释放。
namespace System
{
public interface IDisposable
{
void Dispose();
}
}
如果Subscribe的返回值是IDisposable,那么就可以终止流的订阅,并释放流。
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
如上,可以通过调用Dispose来终止订阅。这里需要注意一点,如果使用Dispose来终止流的订阅,那么OnCompleted将不会被出发。所以,如果你在OnCompleted中写了停止流时的一些触发处理,那么使用Dispose释放流之后,是不会运行的。
void Start(){
var subject = new Subject();
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();
}
在使用UniRX的过程中,时刻注意流的生命周期是非常必要的。频繁创建和删除对象会导致应用性能下降。
在对流进行生命周期管理时,你需要意识到,是什么在控制着流的传递,是什么控制着流。事实上,流的实体是Subject,如果这个Subject被销毁,那么,当前流也会被销毁和终止。之前说过,Subscribe是指在Subject上注册的响应订阅的处理函数。也就是说,在在Subject的内部保留着调用函数的列表(以及与该函数相连的方法链)。这也说明了,Subject是流的管理对象。一旦Subject被全部销毁或者终止,那么流也会被销毁和终止。反过来说,只要Subject继续存在,流就还会继续运转。如果你在流开始传递前丢弃流中需要引用的对象,那么流可能会继续往下传递,从而导致应用性能下降,引起内存泄漏,或者应用直接抛出空异常。所以,在使用流的时候,需要特别细心,一定养成不使用的流,及时Dispose或者OnCompleted;
假设有一个动作游戏,游戏思路如下:
倒计时:
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();
}
}
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);
}
}
}
当计时器到达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;
});
}
正如我们之前说过的那样,流的管理者是Subject,由Subject维持着流的传递和运转,就算Player被Destroy销毁,流依然由TimeCounterUniRX中的Subject维持,所以流依然继续保持,当流满足限定条件时,会访问订阅者的对象,但是,订阅者的对象已经被销毁了,所以才会引发空引用异常。结论就是,如果流的生命周期和对象的生命周期不一致,就会导致对象行为出现异常。
处理的方法很简单,那就是当Player对象被销毁时,终止订阅流程就可以了。UniRX提供了多种终止并释放流的方式,下面的例子展示一个最简单的AddTo的使用方式。
void Start()
{
timeCounterUniRX.OnTimeChanged
.Where(x => x == 0)
.Subscribe(_ =>
{
transform.localPosition = Vector3.zero;
}).AddTo(gameObject);
}
使用了AddTo方法指定当前流的生命周期和this.gameobject的生命周期一致,这样一来,便不会出现之前销毁Player对象之后,任然抛出MissReferenceException的问题了。即当Player被销毁之后,当前流的订阅也会被停止。
在流的执行过程中,有三种类型的信息传递:
停止订阅流的方式:
流的生命周期和对象生命周期之间的关系:
更多内容,欢迎访问: