Skip to content

生命周期

AwakeOnEnableStart 执行顺序是什么?

unity-awake-onenable-start-order

单个 active GameObject 上的 enabled 脚本,典型顺序是:

c
Awake -> OnEnable -> Start -> 第一次 Update

底层理解:Unity 在加载或激活脚本实例时,先做实例初始化事件;对象和组件可用后触发启用事件;等进入第一帧更新前,再调用 Start

c
void Awake()
{
    // 初始化自身状态、缓存组件引用
}

void OnEnable()
{
    // 注册事件、开启监听
}

void Start()
{
    // 依赖其他对象 Awake 完成后的初始化
}

void Update()
{
}

几个面试高频细节:

  • Awake:生命周期内只调用一次。
  • OnEnable:每次组件启用且 GameObject active 时都会调用。
  • Start:生命周期内只调用一次,在第一次 Update 前调用。
  • 场景中 active 对象:Start 不会在所有对象的 Awake 完成前执行。
  • 不同 GameObject 之间的 Awake 先后顺序不保证。
  • GameObject 一开始 inactive:Awake 会延后到它第一次 active。
  • 组件 enabled = false 但 GameObject active:Awake 仍会调用,OnEnableStart 会等组件启用后才调用。

推荐职责划分:

Awake:初始化自己,拿组件引用,建立内部状态
OnEnable:注册事件、开启监听、恢复可用状态
Start:依赖其他对象已经 Awake 后的交互初始化
OnDisable:取消订阅、停止监听

面试高分说法:Awake 适合做对象自身初始化,OnEnable 适合做启用期间的注册和监听,Start 适合做跨对象依赖,因为 Unity 保证场景对象的 Awake 会早于 Start。但不要依赖不同对象之间 Awake 的相对顺序,需要顺序就用显式初始化或 Script Execution Order。

参考:Unity Event function execution orderMonoBehaviour.AwakeMonoBehaviour.OnEnableMonoBehaviour.Start

Unity生命周期函数都有哪些?顺序是?

unity-lifecycle-functions-order

Unity 常见生命周期顺序可以按阶段记:

c
Reset / OnValidate
-> Awake
-> OnEnable
-> Start
-> FixedUpdate
-> Update
-> LateUpdate
-> 渲染相关回调
-> OnDisable
-> OnDestroy

更面试化一点说:

c
编辑器阶段:
Reset、OnValidate

初始化阶段:
Awake -> OnEnable -> Start

循环阶段:
FixedUpdate -> 物理模拟
Update -> 普通每帧逻辑
LateUpdate -> 每帧后处理,比如相机跟随

渲染阶段:
OnBecameVisible / OnBecameInvisible
OnPreCull / OnPreRender / OnRenderObject / OnPostRender
OnRenderImage
OnGUI

应用状态:
OnApplicationFocus
OnApplicationPause
OnApplicationQuit

禁用和销毁:
OnDisable
OnDestroy

几个关键细节:

  • Awake:对象实例加载或首次激活时调用,通常生命周期内一次。
  • OnEnable:对象 active 且组件 enabled 时调用,可以反复调用。
  • Start:第一次 Update 前调用,通常生命周期内一次。
  • FixedUpdate:固定时间步,常用于物理相关逻辑。
  • Update:每帧调用,普通游戏逻辑。
  • LateUpdate:所有 Update 后调用,常用于相机跟随、收尾同步。
  • OnDisable:组件禁用或 GameObject inactive 时调用。
  • OnDestroy:对象销毁或场景卸载时调用。

底层理解:这些函数不是你自己主动调的,而是 Unity 的 PlayerLoop 在不同阶段发现 MonoBehaviour 上有对应方法,就按生命周期阶段调用。不同 GameObject 之间同一个回调的先后顺序不要依赖,除非你设置了 Script Execution Order 或自己做显式初始化。

面试高分说法:我一般把生命周期分成初始化、启用、循环、渲染、禁用销毁几段。Awake 做自身初始化,OnEnable/OnDisable 做事件订阅和取消订阅,Start 做依赖其他对象的初始化,Update/FixedUpdate/LateUpdate 分别对应普通帧逻辑、物理逻辑和帧末收尾。

参考:Unity Event function execution order

UpdateFixedUpdateLateUpdate 区别是什么?

update-fixedupdate-lateupdate

Update 跟渲染帧走,FixedUpdate 跟固定物理步长走,LateUpdate 在所有 Update 之后执行,常用于收尾和相机跟随。

FixedUpdate -> 物理模拟 -> Update -> LateUpdate -> 渲染相关

但注意:FixedUpdate 不是每帧一次。它按 Time.fixedDeltaTime 追赶固定时间步,所以一帧里可能:

c
0 次 FixedUpdate
1 次 FixedUpdate
多次 FixedUpdate

区别:

函数调用频率时间参数适合做什么
Update每个渲染帧一次Time.deltaTime输入、普通逻辑、UI、非物理移动
FixedUpdate固定时间步Time.fixedDeltaTime物理、RigidbodyAddForce
LateUpdate每个渲染帧一次,在 UpdateTime.deltaTime相机跟随、帧末同步、收尾逻辑

典型用法:

c
void Update()
{
    // 输入检测放这里更合适
    horizontal = Input.GetAxis("Horizontal");
}

void FixedUpdate()
{
    // 物理移动放这里
    rb.AddForce(Vector3.right * horizontal);
}

void LateUpdate()
{
    // 目标在 Update 中移动完后,相机再跟随
    camera.transform.position = target.position + offset;
}

面试高分说法:Update 是帧驱动,帧率波动会影响调用间隔;FixedUpdate 是固定时间步驱动,主要服务物理系统,可能一帧执行多次或不执行;LateUpdate 在所有 Update 后调用,适合依赖其他对象本帧已经更新完的逻辑,比如相机跟随。

参考:Unity Event function execution order

OnDisableOnDestroy 什么时候调用?

ondisable-vs-ondestroy

OnDisable 是对象或组件退出启用状态时调用;OnDestroy 是对象或组件即将被销毁时调用。

OnDisable 会在这些情况下调用:

c
组件 enabled = false
GameObject.SetActive(false)
父对象 SetActive(false)
组件或 GameObject 被 Destroy
场景卸载
脚本 Domain Reload

OnDestroy 会在这些情况下调用:

c
Destroy(component)
Destroy(gameObject)
场景卸载
切换场景
退出 Play Mode
应用退出

常见顺序:

c
对象正常运行
-> OnDisable
-> OnDestroy

但如果只是禁用组件或隐藏对象:

c
enabled = false
// 只触发 OnDisable,不触发 OnDestroy

gameObject.SetActive(false)
// 触发 OnDisable,不触发 OnDestroy

推荐职责:

c
void OnEnable()
{
    EventBus.OnDamage += HandleDamage;
}

void OnDisable()
{
    EventBus.OnDamage -= HandleDamage;
}

void OnDestroy()
{
    buffer?.Dispose();
}

面试高分说法:OnDisable 适合做“可启用状态”的清理,比如取消事件订阅、停止监听、暂停协程;OnDestroy 适合做最终释放,比如释放非托管资源、Dispose、保存必要状态。销毁一个 active 对象时通常会先 OnDisable 再 OnDestroy,但普通禁用只会触发 OnDisable。

补一个细节:Unity 官方说明 OnDestroy 只会对曾经 active 过的 GameObject 调用;移动端应用被系统强杀时也不一定可靠触发,所以保存关键数据不要只依赖 OnDestroy

参考:MonoBehaviour.OnDisableMonoBehaviour.OnDestroy

脚本执行顺序怎么控制?

unity-script-execution-order-control

Unity 脚本执行顺序可以用 Script Execution Order[DefaultExecutionOrder] 控制,但它只控制同一生命周期阶段里不同脚本类型的相对顺序,不会改变 Awake -> OnEnable -> Start -> Update 这种阶段顺序。

方式一:Project Settings 设置。

c
Edit -> Project Settings -> Script Execution Order

数值越小越先执行:

c
-200 先执行
-100
Default
100
200 后执行

方式二:代码里写特性:

c
using UnityEngine;

[DefaultExecutionOrder(-100)]
public class GameBootstrap : MonoBehaviour
{
    void Awake()
    {
        Debug.Log("Bootstrap Awake first");
    }
}

[DefaultExecutionOrder(100)]
public class EnemySpawner : MonoBehaviour
{
    void Awake()
    {
        Debug.Log("Spawner Awake later");
    }
}

底层理解:

c
Awake 阶段:按 execution order 排序调用
OnEnable 阶段:按 execution order 排序调用
Start 阶段:按 execution order 排序调用
Update 阶段:按 execution order 排序调用

但它不会这样:

c
把 A.Start 提前到 B.Awake 前面

因为生命周期阶段顺序仍然由 Unity 的 PlayerLoop 决定。

几个关键坑:

  • 只能控制不同 MonoBehaviour 脚本类型之间的顺序。
  • 同一个脚本挂在多个 GameObject 上,这些实例之间的先后不要依赖。
  • Project Settings 里的设置优先级高于 [DefaultExecutionOrder]
  • [DefaultExecutionOrder] 不会明显显示在 Project Settings 列表里,团队协作时要小心。
  • 大量脚本互相靠执行顺序耦合,会很难维护。

项目里更稳的做法是显式初始化:

c
public class GameBootstrap : MonoBehaviour
{
    [SerializeField] private ConfigSystem config;
    [SerializeField] private SaveSystem save;
    [SerializeField] private BattleSystem battle;

    void Awake()
    {
        config.Init();
        save.Init(config);
        battle.Init(config, save);
    }
}

面试高分说法:Script Execution Order 适合解决少量全局脚本的先后问题,比如 Bootstrap、Input、Manager;但复杂模块依赖我更倾向用显式初始化或依赖注入,因为执行顺序是隐式耦合,规模大了很难维护。

参考:Unity Script Execution OrderDefaultExecutionOrder

场景加载时对象生命周期怎么变化?

unity-scene-load-lifecycle

场景加载时,如果是 LoadSceneMode.Single,旧场景普通对象会被卸载销毁,新场景对象会重新走:

c
Awake -> OnEnable -> sceneLoaded -> Start -> Update

普通切场景大概是:

c
旧场景对象:
OnDisable -> OnDestroy

新场景对象:
Awake -> OnEnable -> SceneManager.sceneLoaded -> Start -> Update

示例:

c
SceneManager.LoadScene("BattleScene", LoadSceneMode.Single);

Single 模式会关闭当前已加载场景,然后加载新场景。所以旧场景里的普通对象会退出生命周期:

c
void OnDisable()
{
    // 取消订阅、停止监听
}

void OnDestroy()
{
    // 最终释放资源
}

新场景里的 active 对象会初始化:

c
void Awake()
{
    // 初始化自身
}

void OnEnable()
{
    // 注册事件
}

void Start()
{
    // 依赖其他对象初始化完成后的逻辑
}

如果是 Additive:

c
SceneManager.LoadScene("UIScene", LoadSceneMode.Additive);

旧场景对象不会被销毁,而是继续存在;新加载进来的场景对象会执行自己的:

c
Awake -> OnEnable -> Start

DontDestroyOnLoad 的对象会在切场景时保留:

c
void Awake()
{
    DontDestroyOnLoad(gameObject);
}

常用于:

全局管理器
背景音乐
网络连接
跨场景数据对象

面试高分说法:场景加载不是简单“换个地图”,本质是旧 Scene 的对象生命周期结束,新 Scene 的对象实例被创建并进入生命周期。Single 模式会让旧场景普通对象走 OnDisable/OnDestroyDontDestroyOnLoad 对象会被移到持久区保留;新场景对象走 Awake/OnEnable,然后 Unity 触发 SceneManager.sceneLoaded,再进入 StartAdditive 则不会卸掉旧场景,只是叠加新场景对象。

参考:SceneManager.sceneLoadedLoadSceneMode.SingleDontDestroyOnLoad

DontDestroyOnLoad 有什么坑?

dontdestroyonload-pitfalls

DontDestroyOnLoad 的坑不在 API 本身,而在于:对象从“场景生命周期”变成了“跨场景常驻生命周期”,所以容易出现重复单例、旧场景引用、事件泄漏和资源不释放。

常见坑:

  1. 重复单例

每个场景都放一个 GameManager,切场景时旧的没销毁,新的又创建:

void Awake()
{
    DontDestroyOnLoad(gameObject);
}

结果可能是音乐播放两份、事件响应两次、网络回调重复。

正确写法一般要防重复:

c
public class GameManager : MonoBehaviour
{
    public static GameManager Instance;

    void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this;
        DontDestroyOnLoad(gameObject);
    }
}
  1. 引用旧场景对象

常驻对象如果缓存了旧场景里的 CameraPlayerUI,切场景后这些对象被销毁,但引用还在:

c
private Transform player; // 旧场景 Player

// 切场景后 player 可能已经失效

更稳的做法是在场景加载完成后重新绑定:

c
void OnEnable()
{
    SceneManager.sceneLoaded += OnSceneLoaded;
}

void OnDisable()
{
    SceneManager.sceneLoaded -= OnSceneLoaded;
}

void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
    // 重新查找或由新场景主动注册
}
  1. 事件、协程、Timer 没清理

DontDestroyOnLoad 对象不会随场景卸载,如果它一直订阅事件或跑协程,可能跨场景继续触发,访问已经销毁的对象。

  1. 子节点也会被保留

官方文档说明:如果目标是 GameObjectComponent,它的 Transform 子节点也会一起保留。所以不要把临时 UI、场景对象、测试节点挂在要保活的对象下面。

  1. 生命周期不会重新来一遍

保活对象切场景后不会重新执行 Awake / Start。如果你把“进入新场景后重新初始化”的逻辑写在 Start,它不会再次执行。

  1. 资源常驻导致内存问题

常驻对象持有的 AudioClip、配置、AssetBundle、Addressables handle、网络连接、Native 资源等,不会因为场景卸载自动释放,需要自己设计释放时机。

面试高分说法:DontDestroyOnLoad 本质是把对象移出普通 Scene 生命周期,放到跨场景持久生命周期里。它适合全局管理器、音乐、网络连接这类真正跨场景对象,但要防重复实例、防止持有旧场景对象、防事件和协程泄漏,并且要明确资源释放策略。

参考:Unity Object.DontDestroyOnLoad

禁用 GameObject 和禁用 Component 区别是什么?

disable-gameobject-vs-component

GameObject.SetActive(false) 是禁用整个对象和子层级;component.enabled = false 是只禁用某个组件。

c
gameObject.SetActive(false); // 整个对象 inactive
this.enabled = false;        // 只禁用当前 MonoBehaviour

核心区别:

对比禁用 GameObject禁用 Component
APISetActive(false)enabled = false
范围整个对象 + 子对象层级单个组件
其他组件一起不参与其他组件继续工作
子对象activeInHierarchy 变 false不影响子对象
Update所有脚本都不再收到只有该脚本不再收到
OnDisable相关脚本都会触发只触发该组件的 OnDisable
协程GameObject inactive 会停止协程MonoBehaviour disabled 不会停止已启动协程

底层理解:Unity 判断一个脚本能不能跑,至少要看两层状态:

c
GameObject.activeInHierarchy == true
Behaviour.enabled == true

所以:

c
gameObject.SetActive(false);

会让这个对象及子对象从层级上变成 inactive,脚本、渲染、碰撞等都不再正常参与。

而:

c
myScript.enabled = false;

只是让这个 MonoBehaviour 不再接收 Update 等回调,GameObject 仍然 active,其他组件仍然可以工作。

注意:不是所有 Component 都有 enabled,比如 Transform 不能禁用。通常能禁用的是 MonoBehaviourRendererColliderCameraLight 这类有启用状态的组件。

面试高分说法:禁用 GameObject 是从层级 active 状态上移除对象,影响它自己和子对象;禁用 Component 是关闭某个组件的行为开关。Unity 的生命周期调用会同时受 activeInHierarchyenabled 影响,所以隐藏整个对象用 SetActive(false),只暂停某段逻辑用 enabled = false

参考:GameObject.SetActiveBehaviour.enabled

OnApplicationPauseOnApplicationFocus 有什么用?

onapplicationpause-vs-focus

OnApplicationFocus 处理“应用有没有焦点”;OnApplicationPause 处理“应用是否暂停/进后台”。

c
void OnApplicationFocus(bool hasFocus)
{
    Debug.Log("是否有焦点: " + hasFocus);
}

void OnApplicationPause(bool pauseStatus)
{
    Debug.Log("是否暂停: " + pauseStatus);
}

区别:

回调含义常见场景常用处理
OnApplicationFocus(false)失去焦点桌面 Alt-Tab、窗口失焦、Editor Game 视图失焦、部分移动端输入法弹出暂停输入、弱化音效、停止前台交互
OnApplicationFocus(true)获得焦点回到窗口或应用重新可交互恢复输入、刷新 UI
OnApplicationPause(true)应用暂停手机 Home、锁屏、切后台保存数据、停心跳、记录离线时间
OnApplicationPause(false)应用恢复从后台回到前台重连、校准服务器时间、刷新资源状态

移动端项目里常这样用:

c
void OnApplicationPause(bool pause)
{
    if (pause)
    {
        SaveGame();
        StopHeartbeat();
        lastPauseTime = DateTime.UtcNow;
    }
    else
    {
        ReconnectIfNeeded();
        SyncServerTime();
        RefreshAfterResume();
    }
}

底层理解:这些不是你主动调用的函数,而是 Unity 收到操作系统或窗口系统的生命周期事件后,再分发给 MonoBehaviourFocus 更偏窗口/输入焦点;Pause 更偏应用运行状态。

面试高分说法:我一般不会把保存进度只放在 OnDestroy,因为移动端应用可能被系统直接杀掉。更可靠的做法是在 OnApplicationPause(true) 时做轻量保存和状态记录,在恢复时做重连、时间同步和 UI 刷新。OnApplicationFocus 更多用于控制输入和前台交互状态。

参考:MonoBehaviour.OnApplicationPauseMonoBehaviour.OnApplicationFocus

ResetOnValidate 什么时候调用?

reset-vs-onvalidate

ResetOnValidate 主要是 Unity 编辑器阶段回调,不是普通运行时生命周期。Reset 用来设置默认值,OnValidate 用来校验和修正 Inspector 里的序列化数据。

c
void Reset()
{
    speed = 5f;
    target = GameObject.FindWithTag("Player")?.transform;
}

void OnValidate()
{
    hp = Mathf.Max(1, hp);
    speed = Mathf.Clamp(speed, 0f, 20f);
}

区别:

回调什么时候调用适合做什么
Reset添加组件时,或 Inspector 右键 Reset 时设置组件默认值、自动填引用
OnValidate脚本加载、Inspector 字段变化、序列化数据变化时校验配置、限制范围、自动修正非法值

Reset 常用于:

c
void Reset()
{
    moveSpeed = 3f;
    attackRange = 2f;
}

意思是:这个组件刚被加到对象上时,帮策划或程序填一套合理默认值。

OnValidate 常用于:

c
[SerializeField] private int maxHp;
[SerializeField] private int currentHp;

void OnValidate()
{
    maxHp = Mathf.Max(1, maxHp);
    currentHp = Mathf.Clamp(currentHp, 0, maxHp);
}

这样 Inspector 里改错值时,编辑器会自动修正。

面试高分说法:Reset 更像“组件初始化模板”,只在添加或重置组件时给默认值;OnValidate 更像“编辑器数据校验器”,字段变化时自动检查配置合法性。它们都不应该承担正式运行时初始化逻辑,运行时初始化还是放在 Awake/Start

坑点:OnValidate 可能调用很频繁,官方也提醒它可能从非主线程调用,所以不要在里面做重逻辑、创建/销毁对象、加载资源或复杂 Unity API 操作。

参考:MonoBehaviour.ResetMonoBehaviour.OnValidate

为什么初始化逻辑不全放在 Start 里?

why-not-all-init-in-start

初始化逻辑不应该全放 Start,因为 Start 太晚、只执行一次、还受 active/enabled 状态影响。更稳的做法是按职责拆到 AwakeOnEnableStart

推荐分工:

c
Awake:初始化自己,让对象进入“可被别人安全访问”的状态
OnEnable:注册事件、开启监听、重置对象池状态
Start:依赖其他对象 Awake 完成后的首次协作初始化

为什么不能全放 Start

  1. Start 太晚

别的对象可能在 AwakeOnEnable 就访问你了:

c
class Player : MonoBehaviour
{
    public int Hp;

    void Start()
    {
        Hp = 100;
    }
}

class Enemy : MonoBehaviour
{
    void Awake()
    {
        // 这里可能读到 Player.Hp 还是默认值 0
    }
}

更适合:

c
void Awake()
{
    Hp = 100;
}
  1. Start 只执行一次

对象池里一个对象会反复启用/禁用:

c
SetActive(true)
SetActive(false)
SetActive(true)

Start 只会在第一次启用后调用一次。每次取出对象都要重置的状态,应该放 OnEnable 或对象池的 OnSpawn 里。

  1. Start 受启用状态影响

如果组件一开始是 disabled,Start 会等组件启用后才执行;如果 GameObject 一开始 inactive,整套生命周期都会延后。所以依赖 Start 做关键初始化,可能导致时序不稳定。

  1. 事件订阅不适合全放 Start

订阅事件通常放 OnEnable,取消订阅放 OnDisable

c
void OnEnable()
{
    EventBus.OnDamage += HandleDamage;
}

void OnDisable()
{
    EventBus.OnDamage -= HandleDamage;
}

这样对象启用才监听,禁用就停止监听,不容易泄漏或重复响应。

面试高分说法:我不会把所有初始化都塞进 Start。Awake 用来保证对象自身状态完整,OnEnable 处理启用期间的注册和重置,Start 处理依赖其他对象 Awake 都完成后的协作初始化。这样能避免跨对象时序问题,也适合对象池和事件生命周期管理。

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

本站访客数0总站访问量0本页访问量0