UnityUnity 生命周期

Posted 是嘟嘟啊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了UnityUnity 生命周期相关的知识,希望对你有一定的参考价值。

文章目录


学习生命周期的时候发现官方出了两个版本的生命周期,旧版好像是5.0以前的,新版是5.0以后的,我自己翻译并整理了一下,画了两个图,希望对大家有帮助。
**注:**图是用Astah画的,想要原版的可以私信我。

旧版Unity生命周期

- 官方旧版生命周期

- 个人整理翻译版本

新版Unity生命周期

- 官方新版生命周期

- 个人整理翻译版本


生命周期函数

1、初始化阶段

Awake(唤醒)

当一个脚本实例被载入时Awake被调用,无论脚本是否可用,只要物体被加载就会调用,Awake常用于在游戏开始之前初始化变量或游戏状态,可以判断当满足条件后执行此脚本(只调用一次)。

OnEnable(当可用)

当对象变为可用或激活状态时此函数被调用。(可多次调用)。

Reset(重置)(Editor)

Reset是在用户点击检视面板的Reset按钮或者首次添加该组件时被调用。此函数只在编辑模式下被调用。Reset最常用于在检视面板中给定一个最常用的默认值。

Start(开始)

物体载入且脚本对象启用时被调用1次,常用于数据或逻辑对象初始化(只调用一次)。
Start仅在Update函数第一次被调用前调用,且只会在脚本实例启用时被调用一次。
Awake总是在Start之前执行。可以按需调整延迟初始化代码。

2、物理阶段

FixedUpdate(固定更新)

FixedUpdate基于一个可靠的定时器被调用,独立于帧率之外。如果固定的时间步长小于实际的帧更新时间,那么每帧的物理循环可能会发生不止一次。处理物体的物理属性(Rigidbody、Force、Collider)或者输入事件时,需要用FixedUpdate代替Update,以使物体的物理表现更平滑。实际上,FixedUpdate并不是真的按照现实时间间隔执行的,而是按照Timer时间间隔执行的,但Timer并不是真正意义上的现实时间,它的作用是在运行环境下创造一个与现实时间高度相近的变量来实现物理帧的逻辑稳定。因为FixedUpdate的这个特质,强烈建议在此环节只做物理相关的处理,不要把其他类型(如网络帧同步)的处理也放入此步骤。默认频率大概为0.02s,该频率可手动修改。

在这期间的操作

固定更新结束后,系统内部会进行一系列的操作,最重要的莫过于Unity的内部物理更新,这个是真正的物理更新操作执行。
具体执行步骤大概如下:

OnTriggerXXX(触发)

触发器被触发时调用。

OnCollisionXXX(碰撞)

产生碰撞事件时调用。

yield WaitForFixedUpdate(协程:物理帧结束)

当物理帧执行完毕后会跳转到此协程,协程的调用跟方法是不同的,可以理解为在一段代码中设置一个卡点,当程序执行到这个卡点所匹配的时机时卡点后面的代码才会继续执行。

public class Test : MonoBehaviour

    void Awake()
    
        Debug.Log("0");
        StartCoroutine(TestCoroutine());
        Debug.Log("2");
    

    void FixedUpdate()
    
        Debug.Log("3 - FixedUpdate");
    

    IEnumerator TestCoroutine()
    
        Debug.Log("1");
        yield return new WaitForFixedUpdate();
        Debug.Log("4");                      // 当物理帧结束(触发WaitForFixedUpdate)后才会执行这条语句。 
    

物理阶段总结

该阶段的发生与渲染无关,其特性决定了其处理物理事件的功能,使物理展示效果更为平滑,另外固定更新的频率并非真的是固定的,实际的执行会根据CPU轮转时间片产生偏移,但这个偏移基本可以忽略不记。

3、输入事件阶段

鼠标、键盘、触屏、手柄等各类输入事件会在这个阶段触发,这个时间点物理更新已经执行(如果需要物理更新的话),而逻辑更新和渲染并未执行,要了解这个触发的时机,才能更好的掌握代码逻辑。

4、游戏逻辑阶段

Update(更新)

Update是真正的每帧调用的,由于系统性能以及游戏体量的区别,每一帧的刷新频率也是不同的,所以不要过分期待在Update方法中按时完成任务。
Update与FixedUpdate实际上是使用同一个线程的,update在loop中的处理方式是本次更新完毕再根据上一帧到现在的偏移时间判断是否进行下一次更新,Update的本质就是回调函数。只要是回调函数就存在上下文传递的损耗,所以如果想减少回调,可以考虑自己实现一套update机制,使用虚函数来代替update。具体内容可以了解另一篇文章:【Unity】Unity开发进阶(二)自定义Update

判断多个协程点

在yield WaitForFixedUpdate章节已经讲过协程的执行顺序,当Update执行过后,将会到达以下几个协程的触发点,如果在此之前设置了相关的协程,这时就会生效。

  • yield null
  • yield WaitForSeconds
  • yield WWW:这个比较重要,当网络任务执行完成后,会在当前帧的这个时间点执行WWW之后的操作。一般用于异步加载资源。
  • yield StartCoroutine

内部动画更新

在几个协程过后,Unity将进行第二次大规模的系统内部操作,主要的工作内容就是动画的更新,具体内容如下:

LateUpdate(延后更新)

每帧Update方法调用之后会调用本方法。因为游戏开发过程中经常会有一个二次计算的情况,比如主角移动,相机跟着移动。如果相机也在主角移动时跟随,当有物体跟玩家之间产生了相位,就可能会出现抽搐抖动等情况(因为并没有在这一帧逻辑完全结束后调用跟随)。所以LateUpdate的出现能够使程序更加顺畅。

5、渲染阶段

OnWillRenderObject

当即将渲染物体时调用。

OnPreCull

这个函数仅用于宿主为摄像机的脚本。当此摄像机剔除了某个渲染场景时候触发此消息。

OnBecameVisable(即将可见)

当物体即将可见时调用。

OnBecameInvisible(即将不可见)

当物体即将不可见时调用。

OnPreRender(即将渲染)

这个函数仅用于宿主为摄像机的脚本。当此摄像机开始渲染某个场景时候触发此消息。
在所有渲染开始之前调用,这个方法其实是很考究的,我看到大部分网友对于这个方法都是抄了几种用法:加入脚本、设定标题、设定按钮客户端事件、设定控件的状态、加入脚本块。
个人觉得渲染前可以做的事并不仅仅如此,比如是不是可以考虑在渲染前做一些渲染优化,虽然现在已经有很多插件了,但是如果程序设计的好可以考虑自己实现一套优化,这样更贴合自己的程序。

OnRenderObject

这个函数仅用于宿主为摄像机的脚本。当使用Graphics.DrawMeshNow 或者其他函数绘制自己建立的物体渲染完毕时触发。

OnPostRender

这个函数仅用于宿主为摄像机的脚本。当此摄像机范围内所有渲染都完成时候触发此消息。

OnRenderImage

当所有渲染完成image的postprocessing effects(只有pro版支持)后触发。

OnDrawGizmos(Gizmos渲染)

Gizmos一般是为开发者使用的,指的是开发时场景编辑器中所展示的那些相机、线框之类的物体。所以此方法里的内容一般不会需要发布到生产环境中。

GUI渲染

用户界面渲染的工作会在这一步执行。

yield WaitForEndOfFrame(协程:帧结束)

当前帧彻底结束后会执行此协程。协程运行情况如下:

public class Test : MonoBehaviour

    void Awake()
    
        Debug.Log("0");
        StartCoroutine(TestCoroutine());
        Debug.Log("2");
    

    void Update()
    
        Debug.Log("3 - Update");
    

    void LateUpdate()
    
        Debug.Log("4 - LateUpdate");
    

    IEnumerator TestCoroutine()
    
        Debug.Log("1");
        yield return new WaitForEndOfFrame();
        Debug.Log("5");                      // 当帧结束(触发WaitForEndOfFrame)后才会执行这条语句。 
    

6、暂停阶段

OnApplicationPause(应用暂停)

应用暂停时会调用此方法,取消暂停后会从FixedUpdate开始重新执行。

7、退出阶段

OnDestroy(销毁)

当物体被销毁时调用,一般用于清理内存。

OnApplicationQuit(应用退出)

当应用退出时调用,但有时会失效,此方法为不稳定的方法,正常情况下可以用于保存退出前的信息,但最好使用更稳妥的方式,因为此方法有时不会被调用,比如android环境。


更多内容请查看总目录【Unity】Unity学习笔记目录整理

Rx 生命周期管理

参考技术A 先引用下官方文档:

Here is a sequence of numbers:

Another sequence, with characters:

Some sequences are finite while others are infinite, like a sequence of button taps:

序列从生命周期来讲分为两种: 有限序列和无限序列, 有限序列以complete或者error结束,而无限序列永远都不会发送complete或者error事件。

首先明确一点观察者的生命周期肯定是要比被观察者的生命周期要长的,不然怎么观察呢,如果观察者都死掉了,被观察者还在干活,那你怎么观察呢?
对于有限序列还比较好办,在序列结束的时候发个通知告诉观察者就可以了罗。
对于无限序列呢?怎么去管理它的生命周期呢?既要保证观察者的生命周期比被观察者长,又要在恰当的时候销毁掉它。

有限序列在onError或者onComplete的时候,触发dispose操作。

无限序列就比较麻烦了,先分析下类的归属问题

SinkDisposer 持有 Sink 和 Subscription

AnonymousObservableSink持有 SinkDisposer 和 Observer

看上去有点不太理解,没关系对应下面这个具体例子:

SinkDisposer 持有 AnonymousObservableSink 和

闭包产生的Disposable

AnonymousObservableSink: 持有 SinkDisposer 和

闭包产生的AnonymousObserver.

这样的话Sink持有SinkDisposer, SinkDisposer又持有Sink形成闭环,循环引用下内存永远不会释放。

在Debug模式下会跟踪observable的事件信息和生命周期,这里可以看到在completed情况下,成功触发dispose

这里可以看到没有dispose,现在这种情况下已经造成内存泄漏,这块内存永远不会释放了。
这就是为什么官方推荐你使用下面这种写法的原因:

通过DisposeBag管理观察者的生命的周期,DisposeBag使用数组存放Disposable,在被释放前,会调用所有Disposable.dispose操作,达到释放内存的目的。再来看看dispose具体干了些啥:

首先SinkDisposer 会调用 sink.dispose()
subscription.dispose() 紧接着将其置空,置空之后这个引用环就解开了,自然对象就会被释放掉。

在任何时候使用 subscribe 都必须将其加入到 DisposeBag 中,否则就有可能造成内存泄漏,这样还带来一个好处你无需关注序列到底是有限序列还是无限序列,反正内存会最终释放。

以上是关于UnityUnity 生命周期的主要内容,如果未能解决你的问题,请参考以下文章

UnityUnity学习笔记目录整理

UnityUnity配置PicoXR环境

unity中常用脚本生命周期全解

[IoC容器Unity]第二回:Lifetime Managers生命周期

Unity脚本生命周期与执行顺序

Unity 生命周期