UI(四) - UGUI核心源码剖析
创始人
2024-06-03 14:24:29
0

UGUI核心源码剖析

我们依然从文件夹结构下手,从最容易看懂的地方下手,寻找某块之间的划分,我们先来看下核心部分的文件结构,如下图:

从图中可以看出,以文件夹为单位,拆分模块有,Culling(裁剪), Layout(布局), MaterialModifiers(材质球修改器), SpecializedCollections(收集), Utility(实用工具), VertexModifiers(顶点修改器),我们下面就来分析这些模块.

Culling 裁剪模块

Culling 里是对模型裁剪的工具类,大都用在了 Mask 遮罩上,只有 Mask 才有裁剪的需求。

里面四个文件,其中一个是静态类,一个是接口类。

代码不多,但其中Clipping 类中有2个函数比较重要,常常被用在Mask的裁剪上,如下:


public static Rect FindCullAndClipWorldRect(List rectMaskParents, out bool validRect)
{if (rectMaskParents.Count == 0){validRect = false;return new Rect();}var compoundRect = rectMaskParents[0].canvasRect;for (var i = 0; i < rectMaskParents.Count; ++i)compoundRect = RectIntersect(compoundRect, rectMaskParents[i].canvasRect);var cull = compoundRect.width <= 0 || compoundRect.height <= 0;if (cull){validRect = false;return new Rect();}Vector3 point1 = new Vector3(compoundRect.x, compoundRect.y, 0.0f);Vector3 point2 = new Vector3(compoundRect.x + compoundRect.width, compoundRect.y + compoundRect.height, 0.0f);validRect = true;return new Rect(point1.x, point1.y, point2.x - point1.x, point2.y - point1.y);
}private static Rect RectIntersect(Rect a, Rect b)
{float xMin = Mathf.Max(a.x, b.x);float xMax = Mathf.Min(a.x + a.width, b.x + b.width);float yMin = Mathf.Max(a.y, b.y);float yMax = Mathf.Min(a.y + a.height, b.y + b.height);if (xMax >= xMin && yMax >= yMin)return new Rect(xMin, yMin, xMax - xMin, yMax - yMin);return new Rect(0f, 0f, 0f, 0f);
}

上述中,Clipping 类的函数里,第一个函数 FindCullAndClipWorldRect 的意义是,将很多 RectMask2D 重叠部分,计算出它们的重叠部分的区域。第二个函数 RectIntersect 为第一函数提供了计算服务,其意义是计算两个矩阵的重叠部分。

这两个函数都是静态函数,可以视为工具函数,直接调用就可以,不需要实例化。

Layout 布局模块

我们从文件夹结构可以看出,Layout 主要功能都是布局方面,包括横向布局,纵向布局,方格布局等等。总共12个文件,有9个带有 Layout 字样,它们都是处理布局的。

除了处理布局内容以外,其余3个文件,CanvasScaler,AspectRatioFitter,ContentSizeFitter 则是调整自适应功能。

从 ContentSizeFitter,AspectRatioFitter 都带有 Fitter 字样可以了解到,它们的功能都是处理自适应。其中 ContentSizeFitter 处理的是内容自适应的, 而 AspectRatioFitter 处理的是朝向自适应的,包括以长度为基准的,以宽度为基准的,以父节点为基准的,以外层父节点为基准的自适应,四种类型的自适应方式。

另外 CanvasScaler 做的功能非常重要,它操作的是 Canvas 整个画布针对不同屏幕进行的自适应调整。

我们着重来看看 CanvasScaler 里的代码,其CanvasScaler的核心函数:


protected virtual void HandleScaleWithScreenSize()
{Vector2 screenSize = new Vector2(Screen.width, Screen.height);float scaleFactor = 0;switch (m_ScreenMatchMode){case ScreenMatchMode.MatchWidthOrHeight:{// We take the log of the relative width and height before taking the average.// Then we transform it back in the original space.// the reason to transform in and out of logarithmic space is to have better behavior.// If one axis has twice resolution and the other has half, it should even out if widthOrHeight value is at 0.5.// In normal space the average would be (0.5 + 2) / 2 = 1.25// In logarithmic space the average is (-1 + 1) / 2 = 0float logWidth = Mathf.Log(screenSize.x / m_ReferenceResolution.x, kLogBase);float logHeight = Mathf.Log(screenSize.y / m_ReferenceResolution.y, kLogBase);float logWeightedAverage = Mathf.Lerp(logWidth, logHeight, m_MatchWidthOrHeight);scaleFactor = Mathf.Pow(kLogBase, logWeightedAverage);break;}case ScreenMatchMode.Expand:{scaleFactor = Mathf.Min(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);break;}case ScreenMatchMode.Shrink:{scaleFactor = Mathf.Max(screenSize.x / m_ReferenceResolution.x, screenSize.y / m_ReferenceResolution.y);break;}}SetScaleFactor(scaleFactor);SetReferencePixelsPerUnit(m_ReferencePixelsPerUnit);
}

不同 ScreenMathMode 模式下 CanvasScaler 对屏幕的适应算法,包括优先匹配长或宽的,最小化固定拉伸的,以及最大化固定拉伸三种数学计算方式。其中代码中在优先匹配长或宽算法中,介绍了使用Log和Pow来计算缩放比例可以表现的更好。

MaterialModifiers, SpecializedCollections, Utility

材质球修改器,特殊收集器,实用工具,这三块相对代码量少却很重要,他们是其他模块所依赖的工具。

文件夹结构如下图:

IMaterialModifier 是一个接口类,为Mask 遮罩修改材质球所准备的,但所用方法都需要各自实现。

IndexedSet 是一个容器,在很多核心代码上都有使用,它加速了移除元素的速度,以及加速了元素包含判断。

ListPool是List容器对象池,ObjectPool是普通对象池,很多代码上都用到了它们,对象池让内存利用率更高。

VertexHelper 特别重要,它是用来存储生成 Mesh 网格需要的所有数据,由于在Mesh生成的过程中顶点的生成频率非常高,因此 VertexHelper 存储了 Mesh 的所有相关数据的同时,用上面提到的ListPool和ObjectPool做为对象池来生成和销毁,使得数据高效得被重复利用,不过它并不负责计算和生成 Mesh,计算和生成由各自图形组件来完成,它只为它们提供计算后的数据存储服务。

VertexModifiers

顶点修改器为效果制作提供了更多基础方法和规则。

文件夹结构如下图:

VertexModifiers 模块,主要用于修改图形网格,尤其是在UI元素网格生成完毕后对其进行二次修改。

其中 BaseMeshEffect 是抽象基类,提供所有在修改UI元素网格时所需的变量和接口。

IMeshModifier 是关键接口,在下面的渲染核心类 Graphic 中会获取所有拥有这个接口的组件,然后依次遍历并调用 ModifyMesh 接口来触发改变图像网格的效果。

当前在源码中拥有的二次效果包括,Outline(包边框),Shadow(阴影),PositionAsUV1(位置UV) 都继承了 BaseMeshEffect 基类,并实现了关键接口 ModifyMesh。其中 Outline 继承自 Shadow, 他们的共同的关键代码,我们可以重点看一下:


protected void ApplyShadowZeroAlloc(List verts, Color32 color, int start, int end, float x, float y)
{UIVertex vt;var neededCpacity = verts.Count * 2;if (verts.Capacity < neededCpacity)verts.Capacity = neededCpacity;for (int i = start; i < end; ++i){vt = verts[i];verts.Add(vt);Vector3 v = vt.position;v.x += x;v.y += y;vt.position = v;var newColor = color;if (m_UseGraphicAlpha)newColor.a = (byte)((newColor.a * verts[i].color.a) / 255);vt.color = newColor;verts[i] = vt;}
}

此函数作用是,在原有的Mesh顶点基础上,加入新的顶点,这些新的顶点复制了原来的顶点数据,修改颜色并向外扩充,使得原图形外渲染出外描边或者阴影。

核心渲染类

现在我们来看看核心渲染类的奥秘所在。

我们常用的组件 Image,RawImage,Mask,RectMask2D,Text,InputField 中,Image,RawImage,Text 都是继承了 MaskableGraphic ,而 MaskableGraphic 又继承自 Graphic 类,这里 Graphic 类相对比较重要,是基础类也存些核心算法。除了这几个类外 CanvasUpdateRegistry 是存储和管理所有可绘制元素的管理类也是个蛮重要的类,我们会在下面的内容中介绍。

我们首先来看 Graphic 核心部分,它有两个地方比较重要,这两个地方揭示了 Graphic 的运作机制。

第一个如下:


public virtual void SetAllDirty()
{SetLayoutDirty();SetVerticesDirty();SetMaterialDirty();
}public virtual void SetLayoutDirty()
{if (!IsActive())return;LayoutRebuilder.MarkLayoutForRebuild(rectTransform);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();
}

上述代码中,SetAllDirty 设置并通知元素需要重新布局、重新构建网格、以及重新构建材质球。 它通知 LayoutRebuilder 布局管理类进行重新布局,在 LayoutRebuilder.MarkLayoutForRebuild 中它调用 CanvasUpdateRegistry.TryRegisterCanvasElementForLayoutRebuild 加入重构队伍,最终重构布局。

SetLayoutDirty、SetVerticesDirty、SetMaterialDirty 都调用了 CanvasUpdateRegistry.RegisterCanvasElementForGraphicRebuild,它被调用时可以认为是通知它去重新重构Mesh,但它并没有立即重新构建,而是将需要重构的元件数据加入到IndexedSet容器中,等待下次重构。注意,CanvasUpdateRegistry 只负责重构Mesh网格,并不负责渲染和合并。我们来看看 CanvasUpdateRegistry 的RegisterCanvasElementForGraphicRebuild 函数部分:


public static void RegisterCanvasElementForGraphicRebuild(ICanvasElement element)
{instance.InternalRegisterCanvasElementForGraphicRebuild(element);
}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;}if (m_GraphicRebuildQueue.Contains(element))return false;m_GraphicRebuildQueue.Add(element);return true;
}

上述代码中,InternalRegisterCanvasElementForGraphicRebuild 将元素放入重构队列中等待下一次重构。

以及重构时的逻辑:


private static readonly Comparison s_SortLayoutFunction = SortLayoutList;
private void PerformUpdate()
{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].LayoutComplete();instance.m_GraphicRebuildQueue.Clear();m_PerformingGraphicUpdate = false;
}

上述代码中,PerformUpdate 为 CanvasUpdateRegistry 在重构调用中的逻辑。先将需要重新布局的元素取出来一个个调用Rebuild 函数重构,再对布局后的元素进行裁剪,裁剪后对布局中每个需要重构的元素取出来调用 Rebuild 函数进行重构,最后做一些清理的事务。

我们再来看看 Graphic 另一个重要的函数,即执行网格构建函数。

如下代码:


private void DoMeshGeneration()
{if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)OnPopulateMesh(s_VertexHelper);elses_VertexHelper.Clear(); // clear the vertex helper so invalid graphics dont draw.var components = ListPool.Get();GetComponents(typeof(IMeshModifier), components);for (var i = 0; i < components.Count; i++)((IMeshModifier)components[i]).ModifyMesh(s_VertexHelper);ListPool.Release(components);s_VertexHelper.FillMesh(workerMesh);canvasRenderer.SetMesh(workerMesh);
}

此段代码是 Graphic 构建 Mesh 的部分,先调用OnPopulateMesh创建自己的Mesh网格,然后调用所有需要修改 Mesh 的修改者(IMeshModifier)也就是网格后处理组件(描边等效果组件)进行修改,最后放入 CanvasRenderer 。

其中 CanvasRenderer 是每个绘制元素都必须有的组件,它是画布与渲染的连接组件,通过 CanvasRenderer 我们才能把网格绘制到 Canvas 画布上去。

这里使用 VertexHelper 是为了节省内存和CPU消耗,它内部采用List容器对象池,将所有使用过的废弃的数据都存储在里pool池子的容器中,当需要时再拿旧的继续使用。

public class VertexHelper : IDisposable
{private List m_Positions = ListPool.Get();private List m_Colors = ListPool.Get();private List m_Uv0S = ListPool.Get();private List m_Uv1S = ListPool.Get();private List m_Normals = ListPool.Get();private List m_Tangents = ListPool.Get();private List m_Indicies = ListPool.Get();
}

上述代码为 VertexHelper 的定义部分。

组件中,Image, RawImage, Text 都override重写了 OnPopulateMesh 函数。

    protected override void OnPopulateMesh(VertexHelper toFill)

因为这些需要有自己自定义的网格样式,构建不同类型的画面。

其实 CanvasRenderer 和 Canvas 才是合并Mesh网格的关键,但 CanvasRenderer 和 Canvas 并没有开源出来。并且从源码上看,他们是 C++ 编写的,从另外dll或so引进来。

我试图通过查找反编译的代码查看相关内容,也没有找到,我们无法获得这部分的源码。但仔细一想,也差不多能想个大概。合并部分无非就是每次重构时获取 Canvas 下面所有的 CanvasRenderer 实例,将它们的 Mesh 合并起来,仅此而已。因此关键还是要看,如何减少重构次数,以及提高内存,CPU使用效率。

除了 Graphic类,遮罩部分也是我们非常关心的问题,我们继续看 Mask 遮罩部分的核心部分:

var maskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Replace, CompareFunction.Always, m_ShowMaskGraphic ? ColorWriteMask.All : 0);
StencilMaterial.Remove(m_MaskMaterial);
m_MaskMaterial = maskMaterial;var unmaskMaterial = StencilMaterial.Add(baseMaterial, 1, StencilOp.Zero, CompareFunction.Always, 0);
StencilMaterial.Remove(m_UnmaskMaterial);
m_UnmaskMaterial = unmaskMaterial;
graphic.canvasRenderer.popMaterialCount = 1;
graphic.canvasRenderer.SetPopMaterial(m_UnmaskMaterial, 0);return m_MaskMaterial;

从上述代码中看出来,Mask 组件调用了模板材质球构建了一个自己的材质球,因此它使用了实时渲染中的模板方法来裁切不需要显示的部分,所有在 Mask 组件后面的物体都会进行裁切。我们可以说 Mask 是在 GPU 中做的裁切,使用的方法是着色器中的模板方法。

不过 RectMask2D 并不和 Mask 一样。我们来看 RectMask2D 核心部分源码:


public virtual void PerformClipping()
{// if the parents are changed// or something similar we// do a recalculate hereif (m_ShouldRecalculateClipRects){MaskUtilities.GetRectMasksForClip(this, m_Clippers);m_ShouldRecalculateClipRects = false;}// get the compound rects from// the clippers that are validbool validRect = true;Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);if (clipRect != m_LastClipRectCanvasSpace){for (int i = 0; i < m_ClipTargets.Count; ++i)m_ClipTargets[i].SetClipRect(clipRect, validRect);m_LastClipRectCanvasSpace = clipRect;m_LastClipRectValid = validRect;}for (int i = 0; i < m_ClipTargets.Count; ++i)m_ClipTargets[i].Cull(m_LastClipRectCanvasSpace, m_LastClipRectValid);
}

上述源码中我们可以看到,RectMask2D 会先计算并设置裁切的范围,再对所有子节点调用裁切操作。其中:

    MaskUtilities.GetRectMasksForClip(this, m_Clippers);

获取了所有有关联的 RectMask2D 遮罩范围,然后

    Rect clipRect = Clipping.FindCullAndClipWorldRect(m_Clippers, out validRect);

计算了需要裁切的部分,实际上是计算了不需要裁切的部分,其他部分都进行裁切。最后

for (int i = 0; i < m_ClipTargets.Count; ++i)m_ClipTargets[i].SetClipRect(clipRect, validRect);

对所有需要裁切的UI元素,进行裁切操作。其中 SetClipRect 裁切操作的源码,如下:


public virtual void SetClipRect(Rect clipRect, bool validRect)
{if (validRect)canvasRenderer.EnableRectClipping(clipRect);elsecanvasRenderer.DisableRectClipping();
}

最后操作是在 CanvasRenderer 中进行的。前面说过 CanvasRenderer 是我们无法得知内容。不过我们可以想到这里面的内容,计算两个四边形的相交点,再组合成裁切后的内容。

至此我们把 UGUI 的源代码都剖析完毕了。其实并没有高深的算法或者技术。所有核心部分都围绕着,如何构建Mesh,谁将重构,以及如何裁切的问题上。很多性能关键在于,如何减少重构次数,以及提高内存和CPU的使用效率。

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...