Appearance
生命周期
Awake、OnEnable、Start 执行顺序是什么?
单个 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仍会调用,OnEnable和Start会等组件启用后才调用。
推荐职责划分:
Awake:初始化自己,拿组件引用,建立内部状态
OnEnable:注册事件、开启监听、恢复可用状态
Start:依赖其他对象已经 Awake 后的交互初始化
OnDisable:取消订阅、停止监听面试高分说法:Awake 适合做对象自身初始化,OnEnable 适合做启用期间的注册和监听,Start 适合做跨对象依赖,因为 Unity 保证场景对象的 Awake 会早于 Start。但不要依赖不同对象之间 Awake 的相对顺序,需要顺序就用显式初始化或 Script Execution Order。
参考:Unity Event function execution order、MonoBehaviour.Awake、MonoBehaviour.OnEnable、MonoBehaviour.Start
Unity生命周期函数都有哪些?顺序是?
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
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 | 物理、Rigidbody、AddForce |
LateUpdate | 每个渲染帧一次,在 Update 后 | Time.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
OnDisable 和 OnDestroy 什么时候调用?
OnDisable 是对象或组件退出启用状态时调用;OnDestroy 是对象或组件即将被销毁时调用。
OnDisable 会在这些情况下调用:
c
组件 enabled = false
GameObject.SetActive(false)
父对象 SetActive(false)
组件或 GameObject 被 Destroy
场景卸载
脚本 Domain ReloadOnDestroy 会在这些情况下调用:
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.OnDisable、MonoBehaviour.OnDestroy
脚本执行顺序怎么控制?
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 Order、DefaultExecutionOrder
场景加载时对象生命周期怎么变化?
场景加载时,如果是 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 -> StartDontDestroyOnLoad 的对象会在切场景时保留:
c
void Awake()
{
DontDestroyOnLoad(gameObject);
}常用于:
全局管理器
背景音乐
网络连接
跨场景数据对象面试高分说法:场景加载不是简单“换个地图”,本质是旧 Scene 的对象生命周期结束,新 Scene 的对象实例被创建并进入生命周期。Single 模式会让旧场景普通对象走 OnDisable/OnDestroy,DontDestroyOnLoad 对象会被移到持久区保留;新场景对象走 Awake/OnEnable,然后 Unity 触发 SceneManager.sceneLoaded,再进入 Start。Additive 则不会卸掉旧场景,只是叠加新场景对象。
参考:SceneManager.sceneLoaded、LoadSceneMode.Single、DontDestroyOnLoad
DontDestroyOnLoad 有什么坑?
DontDestroyOnLoad 的坑不在 API 本身,而在于:对象从“场景生命周期”变成了“跨场景常驻生命周期”,所以容易出现重复单例、旧场景引用、事件泄漏和资源不释放。
常见坑:
- 重复单例
每个场景都放一个 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);
}
}- 引用旧场景对象
常驻对象如果缓存了旧场景里的 Camera、Player、UI,切场景后这些对象被销毁,但引用还在:
c
private Transform player; // 旧场景 Player
// 切场景后 player 可能已经失效更稳的做法是在场景加载完成后重新绑定:
c
void OnEnable()
{
SceneManager.sceneLoaded += OnSceneLoaded;
}
void OnDisable()
{
SceneManager.sceneLoaded -= OnSceneLoaded;
}
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 重新查找或由新场景主动注册
}- 事件、协程、Timer 没清理
DontDestroyOnLoad 对象不会随场景卸载,如果它一直订阅事件或跑协程,可能跨场景继续触发,访问已经销毁的对象。
- 子节点也会被保留
官方文档说明:如果目标是 GameObject 或 Component,它的 Transform 子节点也会一起保留。所以不要把临时 UI、场景对象、测试节点挂在要保活的对象下面。
- 生命周期不会重新来一遍
保活对象切场景后不会重新执行 Awake / Start。如果你把“进入新场景后重新初始化”的逻辑写在 Start,它不会再次执行。
- 资源常驻导致内存问题
常驻对象持有的 AudioClip、配置、AssetBundle、Addressables handle、网络连接、Native 资源等,不会因为场景卸载自动释放,需要自己设计释放时机。
面试高分说法:DontDestroyOnLoad 本质是把对象移出普通 Scene 生命周期,放到跨场景持久生命周期里。它适合全局管理器、音乐、网络连接这类真正跨场景对象,但要防重复实例、防止持有旧场景对象、防事件和协程泄漏,并且要明确资源释放策略。
参考:Unity Object.DontDestroyOnLoad
禁用 GameObject 和禁用 Component 区别是什么?
GameObject.SetActive(false) 是禁用整个对象和子层级;component.enabled = false 是只禁用某个组件。
c
gameObject.SetActive(false); // 整个对象 inactive
this.enabled = false; // 只禁用当前 MonoBehaviour核心区别:
| 对比 | 禁用 GameObject | 禁用 Component |
|---|---|---|
| API | SetActive(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 不能禁用。通常能禁用的是 MonoBehaviour、Renderer、Collider、Camera、Light 这类有启用状态的组件。
面试高分说法:禁用 GameObject 是从层级 active 状态上移除对象,影响它自己和子对象;禁用 Component 是关闭某个组件的行为开关。Unity 的生命周期调用会同时受 activeInHierarchy 和 enabled 影响,所以隐藏整个对象用 SetActive(false),只暂停某段逻辑用 enabled = false。
参考:GameObject.SetActive、Behaviour.enabled
OnApplicationPause 和 OnApplicationFocus 有什么用?
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 收到操作系统或窗口系统的生命周期事件后,再分发给 MonoBehaviour。Focus 更偏窗口/输入焦点;Pause 更偏应用运行状态。
面试高分说法:我一般不会把保存进度只放在 OnDestroy,因为移动端应用可能被系统直接杀掉。更可靠的做法是在 OnApplicationPause(true) 时做轻量保存和状态记录,在恢复时做重连、时间同步和 UI 刷新。OnApplicationFocus 更多用于控制输入和前台交互状态。
参考:MonoBehaviour.OnApplicationPause、MonoBehaviour.OnApplicationFocus
Reset 和 OnValidate 什么时候调用?
Reset 和 OnValidate 主要是 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.Reset、MonoBehaviour.OnValidate
为什么初始化逻辑不全放在 Start 里?
初始化逻辑不应该全放 Start,因为 Start 太晚、只执行一次、还受 active/enabled 状态影响。更稳的做法是按职责拆到 Awake、OnEnable、Start。
推荐分工:
c
Awake:初始化自己,让对象进入“可被别人安全访问”的状态
OnEnable:注册事件、开启监听、重置对象池状态
Start:依赖其他对象 Awake 完成后的首次协作初始化为什么不能全放 Start?
Start太晚
别的对象可能在 Awake 或 OnEnable 就访问你了:
c
class Player : MonoBehaviour
{
public int Hp;
void Start()
{
Hp = 100;
}
}
class Enemy : MonoBehaviour
{
void Awake()
{
// 这里可能读到 Player.Hp 还是默认值 0
}
}更适合:
c
void Awake()
{
Hp = 100;
}Start只执行一次
对象池里一个对象会反复启用/禁用:
c
SetActive(true)
SetActive(false)
SetActive(true)但 Start 只会在第一次启用后调用一次。每次取出对象都要重置的状态,应该放 OnEnable 或对象池的 OnSpawn 里。
Start受启用状态影响
如果组件一开始是 disabled,Start 会等组件启用后才执行;如果 GameObject 一开始 inactive,整套生命周期都会延后。所以依赖 Start 做关键初始化,可能导致时序不稳定。
- 事件订阅不适合全放 Start
订阅事件通常放 OnEnable,取消订阅放 OnDisable:
c
void OnEnable()
{
EventBus.OnDamage += HandleDamage;
}
void OnDisable()
{
EventBus.OnDamage -= HandleDamage;
}这样对象启用才监听,禁用就停止监听,不容易泄漏或重复响应。
面试高分说法:我不会把所有初始化都塞进 Start。Awake 用来保证对象自身状态完整,OnEnable 处理启用期间的注册和重置,Start 处理依赖其他对象 Awake 都完成后的协作初始化。这样能避免跨对象时序问题,也适合对象池和事件生命周期管理。