CanvasUpdateRegistry

CanvasUpdateRegistry是一个单例,它是 UGUI 与 Canvas 之间的中介,继承了 ICanvasElement 接口的组件都可以注册到它,它监听了 Canvas 即将渲染的事件,并调用已注册组件的 Rebuild 等方法。

image.png

因此,UGUI 中元素(CanvasElements)的渲染并不是各组件单独控制而是统一交由 CanvasUpdateRegistry 处理。

CanvasUpdate

CanvasUpdateRegistry.cs 文件的首部定义了一个枚举类 CanvasUpdate 用于标识 Canvas 更新过程的各个阶段。

总体而言,Canvas 的更新可以分为布局与渲染两个阶段。其中,PrelayoutLayoutPostLayout 皆为布局阶段中的子阶段;而 PreRenderLatePreRender 则是渲染阶段中的子阶段;一个 UI 元素的刷新过程就依照枚举类中标识的流程进行,后续也会通过该枚举值依次调用各阶段的处理函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/// <summary>
/// Values of 'update' called on a Canvas update.
/// </summary>
public enum CanvasUpdate
{
/// <summary>
/// Called before layout.
/// </summary>
Prelayout = 0,
/// <summary>
/// Called for layout.
/// </summary>
Layout = 1,
/// <summary>
/// Called after layout.
/// </summary>
PostLayout = 2,
/// <summary>
/// Called before rendering.
/// </summary>
PreRender = 3,
/// <summary>
/// Called late, before render.
/// </summary>
LatePreRender = 4,
/// <summary>
/// Max enum value. Always last.
/// </summary>
MaxUpdateValue = 5
}

ICanvasElement

ICanvasElement 接口定义了一个元素在 Canvas 上的生命周期,包括 RebuildLayoutCompleteGraphicUpdateCompleteIsDestroyed 等方法。想要在 Canvas 上显示的元素都需要实现该接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/// <summary>
/// This is an element that can live on a Canvas.
/// </summary>
public interface ICanvasElement
{
/// <summary>
/// Rebuild the element for the given stage.
/// </summary>
/// <param name="executing">The current CanvasUpdate stage being rebuild.</param>
void Rebuild(CanvasUpdate executing);

/// <summary>
/// Get the transform associated with the ICanvasElement.
/// </summary>
Transform transform { get; }

/// <summary>
/// Callback sent when this ICanvasElement has completed layout.
/// </summary>
void LayoutComplete();

/// <summary>
/// Callback sent when this ICanvasElement has completed Graphic rebuild.
/// </summary>
void GraphicUpdateComplete();

/// <summary>
/// Used if the native representation has been destroyed.
/// </summary>
/// <returns>Return true if the element is considered destroyed.</returns>
bool IsDestroyed();
}

其中,Rebuild 方法是最重要的方法,它会在 Canvas 的更新过程中被调用,用于更新元素的状态。LayoutCompleteGraphicUpdateComplete 方法则是在布局与渲染阶段结束后被调用,用于通知元素更新完成。

可以看到,Rebuild 方法中有一个 CanvasUpdate 类型的参数 executing,用于标识当前 Canvas 更新的阶段。在 Rebuild 方法中,我们可以根据 executing 的值来判断当前是布局阶段还是渲染阶段,从而执行不同的操作。

CanvasUpdateRegistry

CanvasUpdateRegistry 是一个单例类,它的构造函数中监听了 Canvas.willRenderCanvases 事件,这意味着每当 Canvas 即将渲染时,CanvasUpdateRegistry 都会调用 PerformUpdate 方法,完成 UI 元素的更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/// <summary>
/// A place where CanvasElements can register themselves for rebuilding.
/// </summary>
public class CanvasUpdateRegistry
{
private static CanvasUpdateRegistry s_Instance;

private bool m_PerformingLayoutUpdate;
private bool m_PerformingGraphicUpdate;

/// <summary>
/// Are graphics layouts currently being calculated..
/// </summary>
/// <returns>True if the rebuild loop is CanvasUpdate.Prelayout, CanvasUpdate.Layout or CanvasUpdate.Postlayout</returns>
public static bool IsRebuildingLayout()
{
return instance.m_PerformingLayoutUpdate;
}

/// <summary>
/// Are graphics currently being rebuild.
/// </summary>
/// <returns>True if the rebuild loop is CanvasUpdate.PreRender or CanvasUpdate.Render</returns>
public static bool IsRebuildingGraphics()
{
return instance.m_PerformingGraphicUpdate;
}

private readonly IndexedSet<ICanvasElement> m_LayoutRebuildQueue = new IndexedSet<ICanvasElement>();
private readonly IndexedSet<ICanvasElement> m_GraphicRebuildQueue = new IndexedSet<ICanvasElement>();

protected CanvasUpdateRegistry()
{
Canvas.willRenderCanvases += PerformUpdate;
}

/// <summary>
/// Get the singleton registry instance.
/// </summary>
public static CanvasUpdateRegistry instance
{
get
{
if (s_Instance == null)
s_Instance = new CanvasUpdateRegistry();
return s_Instance;
}
}

/// ...
}

其中声明了两个 bool 类型的变量 m_PerformingLayoutUpdatem_PerformingGraphicUpdate,用于标识当前是否正在执行布局更新与渲染更新。并且向外提供了两个静态函数 IsRebuildingLayoutIsRebuildingGraphics,用于判断当前是否正在进行布局更新与渲染更新。

m_LayoutRebuildQueuem_GraphicRebuildQueue 分别用于存储需要进行布局更新与渲染更新的元素。

这两个队列都是 IndexedSet<ICanvasElement> 类型,关于 IndexedSet 容器已经在 UGUI-2:IndexedSet 中分析过,是一种支持快速查找与删除的数据结构。

接下来是两个辅助函数,用于判断元素是否可以进行更新与清除无效元素。

1
2
3
4
5
6
7
8
9
10
private bool ObjectValidForUpdate(ICanvasElement element)
{
var valid = element != null;

var isUnityObject = element is Object;
if (isUnityObject)
valid = (element as Object) != null; //Here we make use of the overloaded UnityEngine.Object == null, that checks if the native object is alive.

return valid;
}

ObjectValidForUpdate 函数用于判断元素是否可以进行更新,如果元素不为空且未被销毁,则返回 true。这里使用到了 UnityEngine.Object 类的 == 运算符重载,用于判断对象是否被销毁。虽然看上去是做了两次 != null 的判断,但实际上调用的是不同的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private void CleanInvalidItems()
{
// So MB's override the == operator for null equality, which checks
// if they are destroyed. This is fine if you are looking at a concrete
// mb, but in this case we are looking at a list of ICanvasElement
// this won't forward the == operator to the MB, but just check if the
// interface is null. IsDestroyed will return if the backend is destroyed.

for (int i = m_LayoutRebuildQueue.Count - 1; i >= 0; --i)
{
var item = m_LayoutRebuildQueue[i];
if (item == null)
{
m_LayoutRebuildQueue.RemoveAt(i);
continue;
}

if (item.IsDestroyed())
{
m_LayoutRebuildQueue.RemoveAt(i);
item.LayoutComplete();
}
}

for (int i = m_GraphicRebuildQueue.Count - 1; i >= 0; --i)
{
var item = m_GraphicRebuildQueue[i];
if (item == null)
{
m_GraphicRebuildQueue.RemoveAt(i);
continue;
}

if (item.IsDestroyed())
{
m_GraphicRebuildQueue.RemoveAt(i);
item.GraphicUpdateComplete();
}
}
}

CleanInvalidItems 函数用于清除无效元素,该函数会遍历布局更新队列与渲染更新队列,如果元素为空或已被销毁,则将其从队列中移除,并调用元素的 LayoutCompleteGraphicUpdateComplete 方法。

这里依然要进行两次判断,首先判断元素是否为空,如果为空则直接移除;如果不为空,则调用元素的 IsDestroyed 方法判断元素是否已被销毁。从注释中可以看出,由于两个队列中存储的是 ICanvasElement 类型的元素,而不是 MonoBehaviour 类型的元素,因此无法直接调用 == 运算符重载,需要调用元素的 IsDestroyed 方法来判断元素是否已被销毁。

看到这里博主有些困惑,在 ObjectValidForUpdate 函数中利用 as Object 进行了类型转换,再调用 == 运算符实现对象是否被销毁的判断,而在 CleanInvalidItems 函数中却直接调用了 IsDestroyed 方法,或许是出于性能的考虑吧。

此外还需要注意一点,CleanInvalidItems 函数中的两个循环均是从后往前遍历的,这与 IndexedSet 容器删除操作的逻辑有关。由于 IndexedSet 删除元素的逻辑为将待删除元素与最后一个元素交换位置,然后删除最后一个元素,从后往前遍历保证了交换至待删除位置的元素一定是有效的,减少了额外的检查操作。

PerformUpdate

接下来就是 CanvasUpdateRegistry 中的核心函数 PerformUpdate了。由上文的分析可知,CanvasUpdateRegistryPerformUpdate 函数注册到了 Canvas.willRenderCanvases 事件中,每当 Canvas 即将渲染时,PerformUpdate 函数就会被调用,完成 UI 元素的更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
private void PerformUpdate()
{
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
CleanInvalidItems();

m_PerformingLayoutUpdate = true;

m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)
{
for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
{
var rebuild = instance.m_LayoutRebuildQueue[j];
try
{
if (ObjectValidForUpdate(rebuild))
rebuild.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, rebuild.transform);
}
}
}

for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
m_LayoutRebuildQueue[i].LayoutComplete();

instance.m_LayoutRebuildQueue.Clear();
m_PerformingLayoutUpdate = false;

// now layout is complete do culling...
ClipperRegistry.instance.Cull();

m_PerformingGraphicUpdate = true;
for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
{
for (var k = 0; k < instance.m_GraphicRebuildQueue.Count; k++)
{
try
{
var element = instance.m_GraphicRebuildQueue[k];
if (ObjectValidForUpdate(element))
element.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, instance.m_GraphicRebuildQueue[k].transform);
}
}
}

for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
m_GraphicRebuildQueue[i].GraphicUpdateComplete();

instance.m_GraphicRebuildQueue.Clear();
m_PerformingGraphicUpdate = false;
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
}

PerformUpdate 函数的逻辑非常清晰,就是分 LayoutGraphic 两个阶段分别对 m_LayoutRebuildQueuem_GraphicRebuildQueue 中的元素进行更新。在更新过程中,会根据当前的阶段调用元素的 Rebuild 方法,完成元素的更新逻辑。

Layout 阶段与 Graphic 阶段结束后,会调用元素的 LayoutCompleteGraphicUpdateComplete 方法,通知元素更新完成。此外,在 Layout 阶段结束后还会调用 ClipperRegistry.instance.Cull() 方法,用于裁剪元素。关于裁剪的部分会在后续的文章中详细介绍,这里只需要知道它是在布局阶段结束后执行的即可。

PerformUpdate 函数中,还有一个比较重要的函数 s_SortLayoutFunction(准确来说是一个委托),用于对 m_LayoutRebuildQueue 中的元素进行排序。这里的排序函数会根据元素的父级数量进行排序,父级数量越少的元素越靠前。这样做的目的是为了保证子元素在父元素之后进行更新,从而保证更新的正确性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static readonly Comparison<ICanvasElement> s_SortLayoutFunction = SortLayoutList;

private static int ParentCount(Transform child)
{
if (child == null)
return 0;

var parent = child.parent;
int count = 0;
while (parent != null)
{
count++;
parent = parent.parent;
}
return count;
}

private static int SortLayoutList(ICanvasElement x, ICanvasElement y)
{
Transform t1 = x.transform;
Transform t2 = y.transform;

return ParentCount(t1) - ParentCount(t2);
}

接着是一些辅助函数,用于向 m_LayoutRebuildQueuem_GraphicRebuildQueue 中添加及移除元素。这些辅助函数均为静态函数,而实际的逻辑则通过 instance 中的对应方法实现。主要包括 RegisterCanvasElementForLayoutRebuildTryRegisterCanvasElementForLayoutRebuildRegisterCanvasElementForGraphicRebuildTryRegisterCanvasElementForGraphicRebuildUnRegisterCanvasElementForRebuild 函数。

UI 元素均可以通过这些函数将自己注册进 CanvasUpdateRegistry 中,从而随着 Canvas 一同更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/// <summary>
/// Try and add the given element to the layout rebuild list.
/// Will not return if successfully added.
/// </summary>
/// <param name="element">The element that is needing rebuilt.</param>
public static void RegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}

/// <summary>
/// Try and add the given element to the layout rebuild list.
/// </summary>
/// <param name="element">The element that is needing rebuilt.</param>
/// <returns>
/// True if the element was successfully added to the rebuilt list.
/// False if either already inside a Graphic Update loop OR has already been added to the list.
/// </returns>
public static bool TryRegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
return instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}

private bool InternalRegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
if (m_LayoutRebuildQueue.Contains(element))
return false;

/* TODO: this likely should be here but causes the error to show just resizing the game view (case 739376)
if (m_PerformingLayoutUpdate)
{
Debug.LogError(string.Format("Trying to add {0} for layout rebuild while we are already inside a layout rebuild loop. This is not supported.", element));
return false;
}*/

return m_LayoutRebuildQueue.AddUnique(element);
}

/// <summary>
/// Try and add the given element to the rebuild list.
/// Will not return if successfully added.
/// </summary>
/// <param name="element">The element that is needing rebuilt.</param>
public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}

/// <summary>
/// Try and add the given element to the rebuild list.
/// </summary>
/// <param name="element">The element that is needing rebuilt.</param>
/// <returns>
/// True if the element was successfully added to the rebuilt list.
/// False if either already inside a Graphic Update loop OR has already been added to the list.
/// </returns>
public static bool TryRegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
return instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}

private bool InternalRegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
if (m_PerformingGraphicUpdate)
{
Debug.LogError(string.Format("Trying to add {0} for graphic rebuild while we are already inside a graphic rebuild loop. This is not supported.", element));
return false;
}

return m_GraphicRebuildQueue.AddUnique(element);
}

/// <summary>
/// Remove the given element from both the graphic and the layout rebuild lists.
/// </summary>
/// <param name="element"></param>
public static void UnRegisterCanvasElementForRebuild(ICanvasElement element)
{
instance.InternalUnRegisterCanvasElementForLayoutRebuild(element);
instance.InternalUnRegisterCanvasElementForGraphicRebuild(element);
}

private void InternalUnRegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
if (m_PerformingLayoutUpdate)
{
Debug.LogError(string.Format("Trying to remove {0} from rebuild list while we are already inside a rebuild loop. This is not supported.", element));
return;
}

element.LayoutComplete();
instance.m_LayoutRebuildQueue.Remove(element);
}

private void InternalUnRegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
if (m_PerformingGraphicUpdate)
{
Debug.LogError(string.Format("Trying to remove {0} from rebuild list while we are already inside a rebuild loop. This is not supported.", element));
return;
}
element.GraphicUpdateComplete();
instance.m_GraphicRebuildQueue.Remove(element);
}

总结

image.png

CanvasUpdateRegistry 是 UGUI 系统中一个非常出色的设计,它是 UGUI 与 Canvas 之间的中介,统一管理了 UI 元素的布局与渲染过程。

CanvasUpdateRegistryPerformUpdate 函数注册到了 Canvas.willRenderCanvases 事件中,每当 Canvas 即将渲染时,PerformUpdate 函数就会被调用。PerformUpdate 函数会遍历 m_LayoutRebuildQueuem_GraphicRebuildQueue 中的元素,根据当前的阶段调用元素的 Rebuild 方法,完成元素的更新逻辑。

所谓阶段,是指 CanvasUpdateRegistry 内部对一个 UI 元素更新过程的划分。总体而言,该过程分为布局与渲染两个阶段,在此基础上又细分为五个子阶段。在更新过程中,会根据当前的阶段调用元素的 Rebuild 方法,完成元素的更新逻辑。

此外,CanvasUpdateRegistry 还对外提供了一系列的静态函数供 UI 元素使用,用于将自己注册进 CanvasUpdateRegistry 或从中移除。