Unity invoking methods on disabled UI GameObjects?

Hi,

I have multiple scenes. First scene has basic UI, second has extensive UI. Most of the canvas UI elements in the second scene are disabled (by disabled I mean I disabled parent GameObject and disabled Canvas component on the parent GO). The first scene loads, I click on the button, then I load second scene (LoadSceneMode.Single).

Here is the issue: for some (unknown to me) reason there is HUGE spike when loading the second scene. I can reproduce this in the editor and also on the device (it causes 20s loading time on the Android phone). I tracked the main cause to the two sources:

  1. Application.LoadLevelAsync -> Loading.AwakeFromLoad
  2. Separate Loading.AwakeFromLoad that outside of the Application.LoadLevelAsync

It seems that on scene loading, Unity is iterating over all UI elements (Images, TMPro) and calling either Awake or OnCanvasHierarchyChanged or OnRectTransformDimensionsChange.

Here is the moment in the profiler with the default scene state (all objects present and UI GameObjects disabled):
9342989--1307045--upload_2023-9-19_16-1-52.png

If I simply delete some of the DISABLED GameObjects with the UI the problem seems to go away (there are still 1000s of calls):
9342989--1307048--upload_2023-9-19_16-7-33.png

In the documentation it clearly states that:
[quote]
Awake is called either when an active GameObject that contains the script is initialized when a Scene loads, or when a previously inactive GameObject is set to active, or after a GameObject created with Object.Instantiate is initialized.
[/quote]

As you can see, nothing stated above applies here. The GameObjects are NOT ACTIVE, I don't enable them and all GameObjects are already instatiated in scene.

[quote]
Awake is called even if the script is a disabled component of an active GameObject
[/quote]

This is an exception, where Awake is called on disabled component, but the GameObject itself has to be active.

If I understand everything correctly, when if I disable parent GameObject, then all children are also treated as disabled GameObjects and all components should be disabled too:
[quote]
A GameObject may be inactive because a parent is not active. ..... Deactivating a GameObject disables each component, including attached renderers, colliders, rigidbodies, and scripts.
[/quote]

What is the cause of this behaviour? I checked everything multiple times, there is nothing that activates the GameObjects when the scene is loading and everything is disabled after the scene is loaded exactly in the same state as it is saved in the editor. Is this simply something that is mislabeled in profiler and is it related to the loading of Sprites?

I use 2022.3.5f (I know there are newer builds, but Unity crashes my whole OS every hour due to memory leak).

TLDR: Scene with disabled UI GameObjects loads slow. Scene with deleted disabled UI GameObjects loads fast.

It seems(although I'm not sure) that even though those gameObjects are disabled, they still "technically" load at the scene load. So my guess is they technically get told to be awake at load, then disable. Which each component down the line, if it has an Awake/OnEnable/OnDisable, is also being called.

If my assumption is correct, the only way to bypass this would be to make things more modular, and only instantiate what you need when you need it(use asset load or resource load pathways). I'll have to dig into this myself, because now you got me curious, lol..

1 Like

I looked into it a bit more and found something else that is interesting:

In the first screenshot in my post there are 5437 Image.OnCanvasHierarchyChanged() calls.
In the second screenshot there are just 830 of them after I deleted specific UI GameObjects.

The difference is 5437 - 830 = 4607. So according to this, I removed 4607 Image components?

The issue is, if I call GetComponentsInChildren() on the parent of the deleted UI GameObjects, it returns the list of just 2 Image components. If I pass the "true" argument to include the inactive ones I get 2237 Image components.

That would mean the OnCanvasHierarchyChanged() is called twice or more on some of the Images. Here is what documentation says about OnCanvasHierarchyChanged:

[quote]
Called when the state of the parent Canvas is changed. When a parent canvas is either enabled, disabled or a nested canvas's OverrideSorting is changed this function is called.
[/quote]


So I tried to investigate this and as we know, the Unity UI is distributed as package (com.unity.ugui@1.0.0) and also we know that class Image inherits from MaskableGraphic and it inherits from the class Graphic.

I started at the Graphic.cs. The first thing I noticed, that there are many calls to the IsActive() method. When I looked at the code it just returns the isActiveAndEnabled:

public virtual bool IsActive()
{
return isActiveAndEnabled;
}

Is there any reason for this? Why would there be a method that duplicates the already present property?

Inside the Graphic class, there is one property that holds reference to the Canvas itself (the one that this component is rendering to).

public Canvas canvas
{
    get
    {
        if (m_Canvas == null)
            CacheCanvas();
        return m_Canvas;
    }
}

The private m_Canvas variable is [NonSerialized] and undefined, so when the scene is loaded it is null. That is why it then jumps to CacheCanvas() and it looks like this:

private void CacheCanvas()
        {
            var list = ListPool<Canvas>.Get();
            gameObject.GetComponentsInParent(false, list);
            if (list.Count > 0)
            {
                // Find the first active and enabled canvas.
                for (int i = 0; i < list.Count; ++i)
                {
                    if (list[i].isActiveAndEnabled)
                    {
                        m_Canvas = list[i];
                        break;
                    }

                    // if we reached the end and couldn't find an active and enabled canvas, we should return null . case 1171433
                    if (i == list.Count - 1)
                        m_Canvas = null;
                }
            }
            else
            {
                m_Canvas = null;
            }

            ListPool<Canvas>.Release(list);
        }

Again, is there any reason for it to look like this? Why they get all parent Canvases with the parameter includeInactive set to false and then iterate over all parent canvases and again check if the canvas is active?Is there any reason for it to not look like this below?

private void CacheCanvas()
        {
            if (!IsActive()) return;

            Canvas parentCanvas = gameObject.GetComponentInParent<Canvas>(false);

            m_Canvas = parentCanvas;
}

If I change the method content to this, it creates zero garbage when the Component and GameObject is disabled. And this means it takes almost 4 times less time.

Also inside the OnCanvasHierarchyChanged() method, they already check for the IsActive(), but before this call they set the m_Canvas to null, so it just creates another opportunity to trigger CacheCanvas() on disabled Components.

I tried this changes and haven't seen any problems so far. I suspect there might be some consequences of this and I believe there might be valid reasons why are the methods written in this way, but for my scenario it lowered the garbage created and also slashed a bit of time on scene loading.

[quote]
Is there any reason for this? Why would there be a method that duplicates the already present property?
[/quote]

It's a virtual method, so any sub class can override IsActive() with additional / different logic.

One strategy for UI is to separate it into multiple canvases, since when anything changes on a canvas the entire canvas is dirtied. Good Unite talk here:

https://www.youtube.com/watch?v=_wxitgdx-UI