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的使用效率。

相关内容

热门资讯

河南省龙隐导游词 河南省龙隐导游词  作为一位出色的导游人员,往往需要进行导游词编写工作,导游词一般是根据实际的游览景...
长春长影世纪城导游词 长春长影世纪城导游词  作为一名乐于为游客排忧解难的导游,总归要编写导游词,导游词具有形象、生动、具...
阳朔聚龙潭景区导游词 阳朔聚龙潭景区导游词(精选5篇)  作为一名默默奉献的导游,就难以避免地要准备导游词,导游词可以加深...
大屿山导游词 大屿山导游词  大屿山岛是香港特区最大的岛屿,其面积比香港岛大近一倍。位于珠江口外。大屿山地势西南高...
湖南天子山导游词 湖南天子山导游词  导语:导游词是导游人员引导游客观光游览时的讲解词,是导游员同游客交流思想,向游客...
重庆大足石刻导游词 重庆大足石刻导游词(精选11篇)  作为一名专门为游客提供帮助的导游,常常需要准备导游词,导游词事实...
安徽宏村景点导游词介绍 安徽宏村景点导游词介绍  作为一名旅游从业人员,就有可能用到导游词,导游词可以加深游客对景点的印象,...
庐山芦林湖导游词 庐山芦林湖导游词  作为一名导游,编写导游词是必不可少的,导游词具有注重口语化、精简凝练、重点突出的...
北海公园九龙壁的导游词 北海公园九龙壁的导游词范文  作为一名导游,时常要开展导游词准备工作,导游词是导游员在游览时为口头表...
介绍那拉提草原导游词 介绍那拉提草原导游词  那拉提”是蒙古语“太阳”的意思,对于名字的由来,有一个小小的传说。以下是“介...
江西省庐山山南太乙村导游词 江西省庐山山南太乙村导游词  各位游客,大家好!欢迎来到太乙村旅游。  去过庐山的人很多,但去过太乙...
旅游圣地大理苍山洱海导游词 旅游圣地大理苍山洱海导游词  作为一名优秀的旅游从业人员,就有可能用到导游词,导游词可以帮助旅游者欣...
蓬莱阁的导游词 蓬莱阁的导游词精选  尊敬的旅客朋友们,大家好啊!欢迎来到具有“人间仙境”美称的蓬莱阁参观旅游。我是...
断桥残雪导游词 断桥残雪导游词范文  在白堤的尽头,到了断桥,全长1公里的白堤就由此而“断”了。  断桥的名字最早取...
丽江概况导游词 丽江概况导游词(通用5篇)  作为一名专门为游客提供帮助的导游,时常需要编写导游词,导游词作为一种解...
故宫讲解导游词 2022故宫讲解导游词(精选23篇)  作为一位出色的导游人员,通常需要用到导游词来辅助讲解,借助导...
山西芮城永乐宫导游词 山西芮城永乐宫导游词  作为一位杰出的导游,往往需要进行导游词编写工作,导游词是导游员进行实地口语导...
杭州雷峰塔经典导游词 杭州雷峰塔经典导游词范文(精选3篇)  作为一位无私奉献的导游,时常要开展导游词准备工作,导游词由引...
兵马俑介绍导游词 兵马俑介绍导游词(通用17篇)  作为一名尽职尽责的导游,总归要编写导游词,借助导游词可以更好地宣传...