[UGUI源码二]Unity UI重建(Rebuild)源码分析
Unity怎么绘制UI元素的?首先我们需要明白一个问题:Unity是怎么绘制UI元素的?
Unity中渲染的物体都是由网格(Mesh)构成的,而网格的绘制单元是图元(点、线、三角面)。在unity中添加一个Image和Text,并且将Shadings Mode设置为Wireframe模式,可以看到一个Image由四个顶点和两个三角面构成,Text也是由许多顶点和三角面构成。
绘制信息都存储在Vertexhelper类中,除了顶点外,还包括法线、UV、颜色、切线以及一些函数,下面是它的部分代码
public class VertexHelper : IDisposable
{
private List<Vector3> m_Positions;
private List<Color32> m_Colors;
private List<Vector4> m_Uv0S;
private List<Vector4> m_Uv1S;
private List<Vector4> m_Uv2S;
private List<Vector4> m_Uv3S;
private List<Vector3> m_Normals;
private List<Vector4> m_Tangents;
private List<int> m_Indices;
public void FillMesh(Mesh mesh)
{
InitializeListIfRequired();
mesh.Clear();
if (m_Positions.Count >= 65000)
throw new ArgumentException(&#34;Mesh can not have more than 65000 vertices&#34;);
mesh.SetVertices(m_Positions);
mesh.SetColors(m_Colors);
mesh.SetUVs(0, m_Uv0S);
mesh.SetUVs(1, m_Uv1S);
mesh.SetUVs(2, m_Uv2S);
mesh.SetUVs(3, m_Uv3S);
mesh.SetNormals(m_Normals);
mesh.SetTangents(m_Tangents);
mesh.SetTriangles(m_Indices, 0);
mesh.RecalculateBounds();
}
}
数据存储好了,那怎么绘制呢?
这是依靠CanvasRenderer来完成的,它听起来可能比较陌生,但实际上当我们在项目中创建的一些UI元素,比如Button、Image、Text时,都包含组件CanvasRenderer,这个类提供了许多关键绘制信息,比如被渲染物体的颜色、材质和Mesh等,主要作用就是渲染包含在Canvas中的UI对象,但是在Inspector界面中并不会展示任何属性。
下面列出了几个比较重要的属性和方法,详情见Unity Documentation: CanvasRenderer。
下面对这些步骤展开详细的讲解。
ICanvasElement
首先是ICanvasElement接口,重建的时候会调用它的Rebuild方法,继承它的类都会对这个函数进行重写,Unity中几乎所有的UI组件都继承自这个接口。
下面是接口中包含的方法。
public interface ICanvasElement
{
// 根据CanvasUpdate的不同阶段重建元素
void Rebuild(CanvasUpdate executing);
// 获取ICanvasElement关联的变换组件
Transform transform { get; }
// 布局重建完成的回调函数
void LayoutComplete();
// 图形重建完成的回调函数
void GraphicUpdateComplete();
// 是否被销毁
bool IsDestroyed();
}
可以看到,Rebuild函数需要提供CanvasUpdate类型的参数,它是一个枚举类型,表示Rebuild的不同阶段。
public enum CanvasUpdate
{
//布局重建前
Prelayout = 0,
//布局重建
Layout = 1,
//布局重建后
PostLayout = 2,
//渲染前(图形重建前)
PreRender = 3,
//PreRender后,渲染前
LatePreRender = 4,
//最大枚举值
MaxUpdateValue = 5
}
CanvasUpdateRegistry
一个继承自ICanvasElement接口的类如果要重建,需要将自身加入到CanvasUpdateRegistry类中的重建队列中(RebuildQueue,并不是数据结构中的队列),CanvasUpdateRegistry中包含两个索引集(IndexedSet,内部使用Dictionary和List存储数据),分别是
[*]IndexedSet<ICanvasElement> m_LayoutRebuildQueue ,布局重建队列,当UI元素的布局需要更新时将其加入队列
[*]IndexedSet<ICanvasElement> m_GraphicRebuildQueue ,图形重建队列,当UI元素的图像需要更新时将其加入队列
该类在构造函数中监听了Canvas的willRenderCanvases事件,这个事件会在渲染前进行每帧调用,函数大致包含以下步骤:
[*]PerformUpdate函数对m_LayoutRebuildQueue中的元素进行排序,依据是父节点的多少。接下来依次将Prelayout、Layout和PostLayout作为参数传递给Rebuild进行布局重建,完成后通知布局队列中的元素重建完成。
[*]调用ClipperRegistry的Cull函数进行裁剪。
[*]进行图形重建,遍历m_GraphicRebuildQueue的值,分别将参数PreRender、LatePreRender作为参数传递给Rebuild函数进行图形重建。
[*]最后通知图形重建完成。
protected CanvasUpdateRegistry()
{
Canvas.willRenderCanvases += PerformUpdate;
}
private void PerformUpdate()
{
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
//清理Queue中值为null或者被销毁的元素
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;
try
{
if (ObjectValidForUpdate(rebuild))
//布局重建,分别传入 Prelayout 、Layout 、PostLayout参数
rebuild.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, rebuild.transform);
}
}
}
//通知布局重建完成
for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
m_LayoutRebuildQueue.LayoutComplete();
instance.m_LayoutRebuildQueue.Clear();
m_PerformingLayoutUpdate = false;
//执行裁剪(cull)操作
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;
if (ObjectValidForUpdate(element))
{
//图形重建,分别传入PreRender、LatePreRender参数
element.Rebuild((CanvasUpdate)i);
}
}
catch (Exception e)
{
Debug.LogException(e, instance.m_GraphicRebuildQueue.transform);
}
}
}
//通知图形重建完成
for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
m_GraphicRebuildQueue.GraphicUpdateComplete();
instance.m_GraphicRebuildQueue.Clear();
m_PerformingGraphicUpdate = false;
UISystemProfilerApi.EndSample<span class="p">(UISystemProfilerApi.SampleType.Layout);
}
不论是布局重建还是图形重建,都是遍历m_LayoutRebuildQueue和m_GraphicRebuildQueue中的元素并调用其Rebuild方法,这些UI元素是怎么被添加进Queue的呢?
对于m_LayoutRebuildQueue,提供了两个公开方法向其添加内容,当元素需要进行布局重建的时候,将调用该函数将自身加入队列,m_GraphicRebuildQueue同样也提供了两个函数。
//向m_LayoutRebuildQueue中添加元素
public static void RegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}
//尝试向m_LayoutRebuildQueue中添加元素
//并返回执行结果(True->成功, False->失败)
public static bool TryRegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
return instance.InternalRegisterCanvasElementForLayoutRebuild(element);
}
//如果队列中不存在element元素,则添加
private bool InternalRegisterCanvasElementForLayoutRebuild(ICanvasElement element)
{
if (m_LayoutRebuildQueue.Contains(element))
return false;
return m_LayoutRebuildQueue.AddUnique(element);
}
//向m_GraphicRebuildQueue中添加元素
public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}
//尝试向m_GraphicRebuildQueue中添加元素
public static bool TryRegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
return instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}
private bool InternalRegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{
if (m_PerformingGraphicUpdate)
{
return false;
}
return m_GraphicRebuildQueue.AddUnique(element);
}
以Graphic为例,我们看一下什么时候会向重建队列中添加元素。
Graphic
以Graphic为例(Image和Text间接继承自它),看一下具体发生了什么。
首先是将自身加入重建队列,这里是通过设置“脏数据”实现的,包括布局(Layout)、材质(Material)和顶点(Vertices)三部分,设置布局为脏,将进行布局重建,设置顶点或材质为脏,则进行图形重建。布局重建会将自身加入m_LayoutRebuildQueue中,图形重建则会将自身加入m_GraphicRebuildQueue中,等待被调用。
public virtual void SetAllDirty()
{
if (m_SkipLayoutUpdate)
{
m_SkipLayoutUpdate = false;
}
else
{
SetLayoutDirty();
}
if (m_SkipMaterialUpdate)
{
m_SkipMaterialUpdate = false;
}
else
{
SetMaterialDirty();
}
SetVerticesDirty();
}
public virtual void SetLayoutDirty()
{
if (!IsActive())
return;
//将元素加入布局重建队列
LayoutRebuilder.MarkLayutForRebuild(rectTransform);
Debug.Log(&#34;Rebuild:&#34; + rectTransform.name);
if (m_OnDirtyLayoutCallback != null)
m_OnDirtyLayoutCallback();
}
public virtual void SetVerticesDirty()
{
if (!IsActive())
return;
m_VertsDirty = true;
//将元素加入图形重建队列
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
if (m_OnDirtyVertsCallback != null)
m_OnDirtyVertsCallback();
}
public virtual void SetMaterialDirty()
{
if (!IsActive())
return;
m_MaterialDirty = true;
//将元素加入图形重建队列
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
if (m_OnDirtyMaterialCallback != null)
m_OnDirtyMaterialCallback();
}
加入重建队列之后,CanvasUpdateRegistry就会在PerformUpdate函数中调用它的Rebuild进行重建。Graphic实现了接口ICanvasElement的Rebuild方法,在满足条件的情况下将更新元素的几何网格(UpdateGeometry)和材质(UpdateMaterial)。
public virtual void Rebuild(CanvasUpdate update)
{
if (canvasRenderer == null || canvasRenderer.cull)
return;
switch (update)
{
case CanvasUpdate.PreRender:
if (m_VertsDirty)
{
UpdateGeometry();
m_VertsDirty = false;
}
if (m_MaterialDirty)
{
UpdateMaterial();
m_MaterialDirty = false;
}
break;
}
}
UpdateGeometry函数用于确定元素的网格(Mesh)信息,这些信息包括顶点、三角面、UV、颜色等,它们将会被填充到s_VertexHelper中,并最终调用canvasRenderer.SetMesh(workerMesh)设置Mesh信息。
//调用该函数将图形的几何网格更新到CanvasRenderer上。
protected virtual void UpdateGeometry()
{
//Image、RawImage、Text会在构造函数中将其设置为false
if (useLegacyMeshGeneration)
{
DoLegacyMeshGeneration();
}
else
{
DoMeshGeneration();
}
}
private void DoMeshGeneration()
{
if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)
//UI元素需要生成顶点时的回调函数,用以填充顶点缓冲区的数据
//其子类重写了这个方法
OnPopulateMesh(s_VertexHelper);
else
s_VertexHelper.Clear();
//获取当前对象是否有IMeshModifier接口,
//Text的描边和阴影都是通过它的ModifyMesh方法实现的
var components = ListPool<Component>.Get();
GetComponents(typeof(IMeshModifier), components);
<span class="err">
for (var i = 0; i < components.Count; i++)
((IMeshModifier)components).ModifyMesh(s_VertexHelper);
ListPool<Component>.Release(components);
s_VertexHelper.FillMesh(workerMesh);
//设置渲染所需的网格信息
canvasRenderer.SetMesh(workerMesh);
}
Image
Image间接继承自Graphic,当它的Sprite发生变化时,会调用SetAllDirty函数
public Sprite sprite
{
get { return m_Sprite; }
set
{
if (m_Sprite != null)
{
if (m_Sprite != value)
{
m_SkipLayoutUpdate = m_Sprite.rect.size.Equals(value ? value.rect.size : Vector2.zero);
m_SkipMaterialUpdate = m_Sprite.texture == (value ? value.texture : null);
m_Sprite = value;
SetAllDirty();
TrackSprite();
}
}
else if (value != null)
{
m_SkipLayoutUpdate = value.rect.size == Vector2.zero;
m_SkipMaterialUpdate = value.texture == null;
m_Sprite = value;
SetAllDirty();
TrackSprite();
}
}
}
设置Sprite大小的时候也会调用
public override void SetNativeSize()
{
if (activeSprite != null)
{
float w = activeSprite.rect.width / pixelsPerUnit;
float h = activeSprite.rect.height / pixelsPerUnit;
rectTransform.anchorMax = rectTransform.anchorMin;
rectTransform.sizeDelta = new Vector2(w, h);
SetAllDirty();
}
}
即对应下图中的SetNativeSize按钮
当然修改Image其他的属性也可能会引发重建,调用的地方太多了,想要进一步了解的同学可以在源码中找到答案。
Text类似,当文本的字体、大小等属性发生变化时,也会引起重建。
总结
以下情形都将进行UI重建,因此在项目中可以针对这些情况进行优化,比如用改变UI的Scale(1->0)来代替改变UI的Enable属性,以Image的Scale代替Slider来进行百分比展示等(内容搬运自UGUI UI重建二三事(二) - 知乎 )
<ul><li data-pid="5qoK5baI">Text控件 文本的内容及颜色变化、设置是否支持富文本、更改换行模式、设置字体最大最小值、变更文本使用的对齐锚点、设置是否通过几何对齐、变更字体大小、变更是否支持水平及垂直溢出、修改行间距、变更字体样式(正常、斜体.....)。
页:
[1]