36.GameObject相关优化

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 的字符串属性做判断或每帧使用,例如 taglayername,因为每次访问都会产生桥接。

若逻辑里需要频繁用到这些值,建议:

  • 缓存:在 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。这类方法底层会做场景遍历和字符串比较,性能差,且依赖字符串,容错性也不好,应尽量减少使用。

推荐做法:

  1. 初始化时查找并缓存:在 Awake/Start 里 Find 一次,存为成员或静态引用,后续直接用缓存。
  2. 拖拽引用:在 Inspector 中直接关联目标 GameObject,避免运行时查找。
  3. 全局对象:需要多处使用的对象,可用单例等方式在启动时登记,需要时从单例取引用(单例模式可参考选修内容)。

减少 SendMessage 相关方法的使用

Unity 提供了 SendMessageBroadcastMessageSendMessageUpwards 等基于字符串 + 反射的消息方法,用来通知其他组件执行某方法。它们会遍历 GameObject 上的组件并做反射查找与调用,每次调用都要重新查找、匹配方法名,效率很低,属于低性能、高开销、不可控的“黑箱通信”,应尽量避免使用。

推荐替代方式:

  1. 缓存组件并直接调用:拿到目标组件引用后,直接调用其公开方法,无反射、无字符串查找。
  2. 事件中心 / 观察者模式:用事件或消息中心做跨脚本、跨对象的解耦调用(如 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

×

喜欢就点赞,疼爱就打赏