Unity中的UGUI源码解析之图形对象(Graphic)(2)-ICanvasElement
在上一篇文章中, 我们对整个Graphic部分做了概述, 这篇文章我们介绍ICanvasElement和CanvasUpdateRegistry.
ICanvasElement是一个接口(Interface). 抽象了能够在画布上显示的元素行为. 文件所在为: UnityEngine.UI/UI/Core/CanvasUpateRegistry.cs
.
相关的辅助类有: enum CanvasUpdate
和class CanvasUpdateRegistry
.
在Unity中, 有以下元素实现了该接口:
即:
- Toggle
- LayoutRebuilder
- ScrollRect
- Graphic
- InputField
- Scrollbar
- MaskableGraphic
- Image
- Slider
- RawImage
- Text
我们会在后面的文章慢慢介绍这些元素, 在本文中, 主要关注接口本身和其辅助类.
enum CanvasUpdate
CanvasUpdate是一个枚举, 本身很简单, 用于表明元素的刷新时机, 主要在重建(rebuild)和刷新(update)中使用.
/// <summary>
/// 在Canvas更新时调用的更新时机
/// </summary>
public enum CanvasUpdate
{
/// <summary>
/// 在布局之前调用
/// </summary>
Prelayout = 0,
/// <summary>
/// 在布局时调用
/// </summary>
Layout = 1,
/// <summary>
/// 在布局之后调用
/// </summary>
PostLayout = 2,
/// <summary>
/// 在渲染之前调用
/// </summary>
PreRender = 3,
/// <summary>
/// 稍后再渲染之前调用
/// </summary>
LatePreRender = 4,
/// <summary>
/// 最大值, 用于数组遍历上界
/// </summary>
MaxUpdateValue = 5
}
interface ICanvasElement
ICanvasElement抽象了在画布上显示的元素的基本行为, 用于元素在重建(rebuild)和刷新(update)的各个时机定义和触发特定的行为.
/// <summary>
/// 可以在画布上的元素行为接口
/// </summary>
public interface ICanvasElement
{
/// <summary>
/// 重新构建给定阶段的元素
/// 可以将元素的更新延迟到画布刷新之前, 降低更新的消耗
/// </summary>
/// <param name="executing"></param>
void Rebuild(CanvasUpdate executing);
/// <summary>
/// 相关联的transform
/// </summary>
Transform transform { get; }
/// <summary>
/// 此元素完成布局时的回调
/// </summary>
void LayoutComplete();
/// <summary>
/// 此元素完成Graphic重新构建时的回调
/// </summary>
void GraphicUpdateComplete();
/// <summary>
/// due to unity overriding null check
/// we need this as something may not be null
/// but may be destroyed
/// 返回元素是否被销毁
/// </summary>
/// <returns></returns>
bool IsDestroyed();
}
class CanvasUpdateRegistry
CanvasUpdateRegistry类是一个单例类, 主要负责驱动UGUI元素的刷新和重建, 是底层的Native代码(Canvas)和UGUI元素之间沟通的桥梁.
CanvasUpdateRegistry十分简单, 主要是维护了两个队列, 一个存放需要在布局更新时(比如位置发生变化时)刷新和重建的UGUI元素, 一个存放需要在图形更新时(也就是渲染内容发生变化时, 比如材质和顶点变化), 然后通过注册一个在画布渲染之前发送的事件, 照着布局更新到图形更新的顺序依次对两个队列进行刷新和重建.
刷新队列是各个UGUI元素自己在合适的时机, 调用CanvasUpdateRegistry单例进行注册和加入的, 在每次刷新之后会清空队列.
主要流程
主要的流程为:
-
在CanvasUpdateRegistry构造函数中注册画布渲染前事件:
Canvas.willRenderCanvases += PerformUpdate;
-
各个UGUI元素在合适的时机注册布局更新队列(
RegisterCanvasElementForLayoutRebuild
)或者图形更新队列(RegisterCanvasElementForGraphicRebuild
) -
触发画布渲染前事件:
PerformUpdate
-
UI系统分析启动:
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
-
清理无效的队列元素(比如那些不再存活的元素):
CleanInvalidItems
-
标记布局更新中:
m_PerformingLayoutUpdate = true;
-
布局队列元素排序:
m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
-
对布局队列中的有效元素(可能在某个时机中变得无效)分别进行各个时机的重建:
Prelayout(0), Layout(1), PostLayout(2);
int i = 0; i <= (int)CanvasUpdate.PostLayout; i++
ObjectValidForUpdate(rebuild)
rebuild.Rebuild((CanvasUpdate)i)
-
对布局队列中的元素分别进行布局完成的回调:
m_LayoutRebuildQueue[i].LayoutComplete()
-
清空布局队列:
m_LayoutRebuildQueue.Clear();
-
清理布局更新中标记:
m_PerformingLayoutUpdate = false;
-
进行剔除(cull):
ClipperRegistry.instance.Cull();
-
标记图形更新中:
m_PerformingGraphicUpdate = true;
-
对图形队列中的有效元素(可能在某个时机中变得无效)分别进行各个时机的重建:
PreRender(3), LatePreRender(4)
var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++
ObjectValidForUpdate(element)
element.Rebuild((CanvasUpdate)i)
-
对图形队列中的元素分别进行图形更新完成的回调:
m_GraphicRebuildQueue[i].GraphicUpdateComplete()
-
清空图形队列:
m_GraphicRebuildQueue.Clear();
-
清理图形更新中标记:
m_PerformingGraphicUpdate= false;
-
UI系统分析结束:
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
基本属性和字段
public class CanvasUpdateRegistry
{
/// <summary>
/// 单例
/// </summary>
private static CanvasUpdateRegistry s_Instance;
/// <summary>
/// 是否在更新布局(位置)
/// </summary>
private bool m_PerformingLayoutUpdate;
/// <summary>
/// 是否在更新图形(渲染)
/// </summary>
private bool m_PerformingGraphicUpdate;
/// <summary>
/// 需要在布局更新时重建的元素(位置)
/// </summary>
private readonly IndexedSet<ICanvasElement> m_LayoutRebuildQueue = new IndexedSet<ICanvasElement>();
/// <summary>
/// 需要在图形更新时重建的元素(渲染)
/// </summary>
private readonly IndexedSet<ICanvasElement> m_GraphicRebuildQueue = new IndexedSet<ICanvasElement>();
/// <summary>
/// 单例
/// </summary>
public static CanvasUpdateRegistry instance
{
get
{
if (s_Instance == null)
s_Instance = new CanvasUpdateRegistry();
return s_Instance;
}
}
}
注册元素和回调
// 构造函数中注册即将开始渲染画布时的回调
protected CanvasUpdateRegistry()
{
Canvas.willRenderCanvases += PerformUpdate;
}
/// <summary>
/// 注册图形元素重建
/// </summary>
/// <param name="element"></param>
public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}
/// <summary>
/// 注册布局元素重建
/// </summary>
/// <param name="element"></param>
public static void RegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}
public static void UnRegisterCanvasElementForRebuild(ICanvasElement element)
{
instance.InternalUnRegisterCanvasElementForLayoutRebuild(element);
instance.InternalUnRegisterCanvasElementForGraphicRebuild(element);
}
工具方法
/// <summary>
/// 是否正在进行布局更新
/// </summary>
/// <returns></returns>
public static bool IsRebuildingLayout()
{
return instance.m_PerformingLayoutUpdate;
}
/// <summary>
/// 是否正在进行图形更新
/// </summary>
/// <returns></returns>
public static bool IsRebuildingGraphics()
{
return instance.m_PerformingGraphicUpdate;
}
/// <summary>
/// 清理无效的元素
/// </summary>
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();
}
}
}
/// <summary>
/// 判断元素是否有效
/// </summary>
/// <param name="element"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 布局元素排序委托, 做成静态成员可以减少函数到委托的转换
/// </summary>
private static readonly Comparison<ICanvasElement> s_SortLayoutFunction = SortLayoutList;
/// <summary>
/// 排序节点
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
private static int SortLayoutList(ICanvasElement x, ICanvasElement y)
{
Transform t1 = x.transform;
Transform t2 = y.transform;
return ParentCount(t1) - ParentCount(t2);
}
执行更新主流程
/// <summary>
/// 执行更新主流程
/// </summary>
private void PerformUpdate()
{
// UI系统分析启动
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
// 清理无效元素
CleanInvalidItems();
// 标记布局更新中
m_PerformingLayoutUpdate = true;
// 排序布局元素(根据父节点数量从小到大的排序)
m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
// 在各个时机分别触发各个布局元素的重建(Prelayout, Layout, PostLayout)
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;
// 布局元素完成更新后, 进行剔除操作(后面的文章单独介绍)
ClipperRegistry.instance.Cull();
// 标记图形元素更新中
m_PerformingGraphicUpdate = true;
// 在各个时机分别触发各个图形元素的重建(PreRender, LatePreRender)
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;
// UI系统分析结束
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
}
举例子: Graphic的实现
/// <summary>
/// 重建接口的实现
/// </summary>
/// <param name="update"></param>
public virtual void Rebuild(CanvasUpdate update)
{
if (canvasRenderer.cull)
return;
// 在渲染之前更新所需内容(顶点或者材质)
switch (update)
{
case CanvasUpdate.PreRender:
if (m_VertsDirty)
{
UpdateGeometry();
m_VertsDirty = false;
}
if (m_MaterialDirty)
{
UpdateMaterial();
m_MaterialDirty = false;
}
break;
}
}
总结
今天介绍了ICanvasElement和其相关的工具类, ICanvasElement抽象了可以在画布上显示的元素的行为, 主要是重建和一些回调, 而CanvasUpdateRegistry则是使用ICanvasElement的主体, 根据注册画布渲染前事件来触发那些需要进行重建的元素(比如元素自己知道某些情况下需要进行重建, 就要把自己注册到CanvasUpdateRegistry)的接口.
本质上CanvasUpdateRegistry的存在是为了统一UGUI元素的刷新, 一方面避免刷新时机的不统一, 一方面也能降低刷新频率. Unity在很多地方都使用了类似的机制, 值得我们借鉴.
好了, 今天的内容就是这些, 希望对大家有所帮助.