直接分析Graphic源码不太直观 ,我们从顶层分析Text控件的运作机制这样可以知道Graphic的运作机制了,首先Text继承关系如下
public class Text : MaskableGraphic, ILayoutElement
public abstract class MaskableGraphic : Graphic, IClippable, IMaskable, IMaterialModifier
我们需要分析Text控件何时会刷新,可以肯定地是Graphic使用了脏标记模式(游戏编程
模式里面有这个设计模式),每当Text的一些属性发生改变后不会立即更新属性,而是将这些变化的属性打上标记,并在某一个时刻一起更新。
鉴于这些类比较复杂我们通过一个简单的行为去分析Text内部的运行机制。我们首先考虑当Text控件的内容发生变化时,Text控件到底执行了那些方法。通过下面代码可以看出,当Text控件的内容发生变化时会将顶点(vertex)以及布局(layout)打上标记。
public virtual string text
{
get
{
return m_Text;
}
set
{
if (String.IsNullOrEmpty(value))
{
if (String.IsNullOrEmpty(m_Text))
return;
m_Text = "";
SetVerticesDirty();
}
else if (m_Text != value)
{
m_Text = value;
SetVerticesDirty();
SetLayoutDirty();
}
}
}
我们继续分析SetVerticesDirty方法。这个方法跳到Graphic类里面来了,这个方法首先设置标志位,然后CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild 方法会向一个队列中添加一个元素,到这里Text的部分就分析完了。但是不对啊,Text控件的内容什么时候渲染更新呢,我们继续分析CanvasUpdateRegistry
return m_GraphicRebuildQueue.AddUnique(element);
public virtual void SetVerticesDirty()
{
if (!IsActive())
return;
m_VertsDirty = true;
CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild(this);
if (m_OnDirtyVertsCallback != null)
m_OnDirtyVertsCallback();
}
接下来我们分析 CanvasUpdateRegistry 类,他的构造函数如下会向Canvas中的willRenderCanvases静态事件添加一个回调,由于Canvas没有源码,这里我推测performUpdate这个回调会每一帧都执行更新。performUpdate这个回调的功能很简单就是根据从m_GraphicRebuildQueue以及m_LayoutRebuildQueue两个队列中取出元素,并执行每一个元素的Rebuild方法,Rebuild方法会更具不同的枚举类型进行不同的操作。Rebuild完成后执行LayoutComplete()与GraphicUpdateComplete()方法。
public enum CanvasUpdate
{
Prelayout,
Layout,
PostLayout,
PreRender,
LatePreRender,
MaxUpdateValue,
}
protected CanvasUpdateRegistry()
{
Canvas.willRenderCanvases += PerformUpdate;
}
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++)
{
UnityEngine.Profiling.Profiler.BeginSample(m_CanvasUpdateProfilerStrings[i]);
for (int j = 0; j < m_LayoutRebuildQueue.Count; j++)
{
var rebuild = m_LayoutRebuildQueue[j];
try
{
if (ObjectValidForUpdate(rebuild))
rebuild.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, rebuild.transform);
}
}
UnityEngine.Profiling.Profiler.EndSample();
}
for (int i = 0; i < m_LayoutRebuildQueue.Count; ++i)
m_LayoutRebuildQueue[i].LayoutComplete();
m_LayoutRebuildQueue.Clear();
m_PerformingLayoutUpdate = false;
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Render);
// now layout is complete do culling...
UnityEngine.Profiling.Profiler.BeginSample(m_CullingUpdateProfilerString);
ClipperRegistry.instance.Cull();
UnityEngine.Profiling.Profiler.EndSample();
m_PerformingGraphicUpdate = true;
for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
{
UnityEngine.Profiling.Profiler.BeginSample(m_CanvasUpdateProfilerStrings[i]);
for (var k = 0; k < m_GraphicRebuildQueue.Count; k++)
{
try
{
var element = m_GraphicRebuildQueue[k];
if (ObjectValidForUpdate(element))
element.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, m_GraphicRebuildQueue[k].transform);
}
}
UnityEngine.Profiling.Profiler.EndSample();
}
for (int i = 0; i < m_GraphicRebuildQueue.Count; ++i)
m_GraphicRebuildQueue[i].GraphicUpdateComplete();
m_GraphicRebuildQueue.Clear();
m_PerformingGraphicUpdate = false;
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Render);
经过上面的分析我们继续来看,Text的Rebuild方法,首先判断脏标记,并执行相应的更新。
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;
}
}
综上所述,我们可以很清楚的知道Text控件某些属性改变后的更新过程。
1.当某些属性改变时,可能时图像(Graphic)或者布局(layout)Text会进行脏标记,并向 CanvasUpdateRegistry 类中的相应队列中添加一个元素(把Text的引用添加进去,Text继承了ICanvasElement接口)。
2.CanvasUpdateRegistry 每一帧会调用PerformUpadate方法,该方法会遍历队列中的元素,并执行ICanvasElement接口的rebuild方法(Text重写了rebuild方法实际执行的时Text的rebuild方法)
3.最后在Text的rebuild方法中会根据脏标记,去更新Text的一些属性。
到这里Text的更新循环已经分析完毕了,现在继续分析Text是如何更新自己的属性,并且渲染到屏幕上。我们只分析Text的Geometry(几何可以说就是顶点)部分的更新,在rebuild中执行了UpdateGeometry方法,这个方法调用几个方法后,最终执行了DoMeshGeneration
方法。s_VertexHelper是一个工具类的实例,可以帮助生成UI的Mesh。然后DoMeshGeneration方法会执行OnPopulateMesh方法向s_VertexHelper更新Mesh,最后通过CanvasRenderer(可以在unity面板找到)组件进行渲染。
private void DoMeshGeneration()
{
if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)
OnPopulateMesh(s_VertexHelper);
else
s_VertexHelper.Clear(); // clear the vertex helper so invalid graphics dont draw.
var components = ListPool<Component>.Get();
GetComponents(typeof(IMeshModifier), components);
for (var i = 0; i < components.Count; i++)
((IMeshModifier)components[i]).ModifyMesh(s_VertexHelper);
ListPool<Component>.Release(components);
s_VertexHelper.FillMesh(workerMesh);
canvasRenderer.SetMesh(workerMesh);
}
protected override void OnPopulateMesh(VertexHelper toFill)
{
if (font == null)
return;
// We don't care if we the font Texture changes while we are doing our Update.
// The end result of cachedTextGenerator will be valid for this instance.
// Otherwise we can get issues like Case 619238.
m_DisableFontTextureRebuiltCallback = true;
Vector2 extents = rectTransform.rect.size;
var settings = GetGenerationSettings(extents);
cachedTextGenerator.PopulateWithErrors(text, settings, gameObject);
// Apply the offset to the vertices
IList<UIVertex> verts = cachedTextGenerator.verts;
float unitsPerPixel = 1 / pixelsPerUnit;
int vertCount = verts.Count;
// We have no verts to process just return (case 1037923)
if (vertCount <= 0)
{
toFill.Clear();
return;
}
Vector2 roundingOffset = new Vector2(verts[0].position.x, verts[0].position.y) * unitsPerPixel;
roundingOffset = PixelAdjustPoint(roundingOffset) - roundingOffset;
toFill.Clear();
if (roundingOffset != Vector2.zero)
{
for (int i = 0; i < vertCount; ++i)
{
int tempVertsIndex = i & 3;
m_TempVerts[tempVertsIndex] = verts[i];
m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
m_TempVerts[tempVertsIndex].position.x += roundingOffset.x;
m_TempVerts[tempVertsIndex].position.y += roundingOffset.y;
if (tempVertsIndex == 3)
toFill.AddUIVertexQuad(m_TempVerts);
}
}
else
{
for (int i = 0; i < vertCount; ++i)
{
int tempVertsIndex = i & 3;
m_TempVerts[tempVertsIndex] = verts[i];
m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
if (tempVertsIndex == 3)
toFill.AddUIVertexQuad(m_TempVerts);
}
}
m_DisableFontTextureRebuiltCallback = false;
}