36.性能优化-CPU-脚本-GameObject
36.1 知识点
更快的空引用检测
开发中经常要对 GameObject、MonoBehaviour 等 Unity 对象做判空,最常见的是用 if (gameObject != null) 再处理。这样写最简单,但对 Unity 自带类型来说并不是效率最高的方式。
更快的写法: 使用 System.Object.ReferenceEquals 做引用判断。
- 从性能上看,
ReferenceEquals消耗更低、执行更快,只做引用地址比较。 - 原因:用
==/!= null会走 Unity 重载的运算符,仍然有桥接访问,存在额外开销;而ReferenceEquals不经过桥接,只在托管侧做引用对比。
使用陷阱: 若已销毁 GameObject 但没有把引用置空,两种方式的结论会不一致。
- 用
!= null:Unity 重载了判空,销毁后即使没置空也会视为“空”,不会进 if。 - 用
ReferenceEquals:只比较引用是否为 null,销毁后若未置空,引用仍非 null,会认为“非空”。
因此用 ReferenceEquals 时,要自己处理好“销毁 + 置空”,否则可能误判。该知识点仅作了解:虽然比直接判空快,但提升很小,且存在上述陷阱,实际可能得不偿失。
示例(演示两种判空在“销毁未置空”时的差异):
// 销毁对象并置空
DestroyImmediate(gameObj);
gameObj = null;
// Unity 重载了判空:即使没有 gameObj = null,销毁后这里也不会进来
if (gameObj != null)
{
print("!=null判断, obj不为空");
}
// ReferenceEquals 只比引用:若没有 gameObj = null,即便调用了DestroyImmediate销毁后仍会打印“obj不为空”
if (!System.Object.ReferenceEquals(gameObj, null))
{
print("ReferenceEquals判断, obj不为空");
}
推荐判空写法(仅当确实需要极致少一次桥接时再考虑):
// 常规写法(可读性好,多数情况推荐)
if (gameObject != null) { }
// 更少桥接的写法(需自行保证销毁后置空)
if (!System.Object.ReferenceEquals(gameObject, null)) { }
避免检索字符串属性
应尽量避免直接用 GameObject 的字符串属性做判断或每帧使用,例如 tag、layer、name,因为每次访问都会产生桥接。
若逻辑里需要频繁用到这些值,建议:
- 缓存:在 Awake/Start 中取一次存到成员变量,后续用缓存判断。
- 用 API 替代:例如用
CompareTag(string)代替直接比gameObject.tag,减少字符串分配与比较成本。
示例:
// 在 Start 中缓存 tag,避免每帧访问 gameObject.tag
private string cachedTag;
void Start()
{
cachedTag = gameObject.tag;
}
// 需要按 tag 判断时,优先用 CompareTag(内部实现更高效)
void Update()
{
if (gameObject.CompareTag("Player")) { }
}
减少 Find 相关方法的使用
GameObject 提供了多种基于字符串的查找方法,例如:
GameObject.Find(string name)GameObject.FindWithTag(string tag)
以及其它类似 API。这类方法底层会做场景遍历和字符串比较,性能差,且依赖字符串,容错性也不好,应尽量减少使用。
推荐做法:
- 初始化时查找并缓存:在 Awake/Start 里 Find 一次,存为成员或静态引用,后续直接用缓存。
- 拖拽引用:在 Inspector 中直接关联目标 GameObject,避免运行时查找。
- 全局对象:需要多处使用的对象,可用单例等方式在启动时登记,需要时从单例取引用(单例模式可参考选修内容)。
减少 SendMessage 相关方法的使用
Unity 提供了 SendMessage、BroadcastMessage、SendMessageUpwards 等基于字符串 + 反射的消息方法,用来通知其他组件执行某方法。它们会遍历 GameObject 上的组件并做反射查找与调用,每次调用都要重新查找、匹配方法名,效率很低,属于低性能、高开销、不可控的“黑箱通信”,应尽量避免使用。
推荐替代方式:
- 缓存组件并直接调用:拿到目标组件引用后,直接调用其公开方法,无反射、无字符串查找。
- 事件中心 / 观察者模式:用事件或消息中心做跨脚本、跨对象的解耦调用(如 Unity 基础小框架中的实现,可选修)。
36.2 知识点代码
Lesson36_性能优化_CPU_脚本_GameObject.cs
using UnityEngine;
public class Lesson36_性能优化_CPU_脚本_GameObject : MonoBehaviour
{
public GameObject gameObj;
// 缓存 tag,避免每帧访问 gameObject.tag 产生桥接
private string cachedTag;
void Start()
{
#region 知识点一 更快的空引用检测
/* 常规判空 if (gameObject != null) 会走 Unity 重载,有桥接开销;
* ReferenceEquals 只做引用比较,无桥接,但需注意:销毁后若不置空,两种判空结果会不一致。*/
DestroyImmediate(gameObj);
gameObj = null;
if (gameObj != null)
{
// 若无 gameObj = null,Unity 重载判空后也不会进这里
print("!=null判断, obj不为空");
}
if (!System.Object.ReferenceEquals(gameObj, null))
{
// 若无 gameObj = null,会打印这条(引用仍未 null)
print("ReferenceEquals判断, obj不为空");
}
#endregion
#region 知识点二 避免检索字符串属性
// tag、layer、name 等字符串属性会产生桥接,建议缓存或用 CompareTag 等 API
cachedTag = gameObject.tag;
#endregion
#region 知识点三 减少 Find 相关方法的使用
// Find/FindWithTag 等会遍历和字符串比较,性能差;建议初始化时查找并缓存、拖拽引用或单例管理
#endregion
#region 知识点四 减少 SendMessage 相关方法的使用
// SendMessage/BroadcastMessage/SendMessageUpwards 基于字符串反射,遍历组件、效率低;
// 建议缓存组件直接调用,或使用事件中心等观察者方式
#endregion
}
void Update()
{
// 按 tag 判断时优先用 CompareTag,而非直接比较 gameObject.tag
if (gameObject.CompareTag("123"))
{
}
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com