文章说明
本篇文章基于NGUI (3.12.0)版本源码下的代码分析,如果代码和大家自己的不同,可能是版本不同。如果文章中分析有误希望大神看见指点迷津,大家好,我就是一个勤勤恳恳爱偷懒的码农,希望和大家一起学习研究。
今天来给大家分享 NGUI 的 UIPanel 源码,主要分享 UIPanel 中关于 UIDrawcall 调用相关部分,如果你之前了解过 “unity 性能优化相关” 东西的话 ,那么其中与 unity 性能相关之 drawcall 优化的主要部分就是在这里,当然了前提是你们的 UI 底层使用的是 NGUI,如果使用的是UGUI,就要去UGUI 源码中分析如何优化 drawcall 。
源码分析
本篇文章主要分享 和 UIDrawcall 调用相关的源码部分,剩下的比如,裁剪,锚点等,放到后面文章中分享哈。前面关于 UIDrawcall 的源码分享中,我提到过几个思考问题:
谁创建的 UIDrawcall ,什么时候创建?
谁调用的 UIDrawcall ,什么时候调用?
谁移除的 UIDrawcall ,什么时候移除?
今天就从这些问题去源码中去寻找答案。
脚本初始化流程
首先要去 UIPanel 的父类 “UIRect” 中去研究一下它的初始化逻辑,接下来我给大家梳理下,UIPanel 初始化主要分为两种情况:
- mStarted == false 时:Awake -----> OnEnable ----> Start —> 调用 OnInit 和 OnStart,将 mStarted 置为 true
- mStarted == true 时:OnEnable ----> 调用 OnInit 和 OnStart
(mStarted 存储在 UIRect 中, 初始化 是 “false”,从源码中看,是用来标记 Start 方法是否执行的标志。)
如下是 UIRect 相关源码:
protected virtual void Awake ()
{
mStarted = false;
mGo = gameObject;
mTrans = transform;
}
protected void Start ()
{
mStarted = true;
OnInit();
OnStart();
}
protected virtual void OnEnable ()
{
#if UNITY_EDITOR
mEnabled = true;
#endif
mUpdateFrame = -1;
if (updateAnchors == AnchorUpdate.OnEnable)
{
mAnchorsCached = false;
mUpdateAnchors = true;
}
if (mStarted) OnInit();
mUpdateFrame = -1;
}
重要属性
UIPanel 中一些重点属性如下:
//UIPanel 中有一个静态变量 list,保存当前项目中创建的所有的 UIPanel 组件。
static public List<UIPanel> list = new List<UIPanel>(); //所有的 UIPanel 都会被放到这个列表中
//这个 UIPanel 管理的 uiwidget 列表,注意 uisprite uitexture 等都是 uiwidget
public List<UIWidget> widgets = new List<UIWidget>();
//这个 UIPanel 创建的 draw call 列表
public List<UIDrawCall> drawCalls = new List<UIDrawCall>();
//缓存的裁剪范围
public Vector4 drawCallClipRange = new Vector4(0f, 0f, 1f, 1f);
UIDrawCall.Clipping mClipping = UIDrawCall.Clipping.None; //裁剪类型
//默认的 SoftClip 模式下的坐标(中心x,中心y,宽度,高度)
Vector4 mClipRange = new Vector4(0f, 0f, 300f, 200f);
// x表示内矩形距离外矩形左右两边的距离,y表示内矩形距离外矩形上下两边的距离
Vector2 mClipSoftness = new Vector2(4f, 4f);
int mDepth = 0; //深度
int mSortingOrder = 0; //渲染排序
bool mRebuild = false; //用于是否重建所有的 drawcall
UIPanel 创建
OnInit():每次激活这个 UIPanel 组件时,OnEnable 执行完后,都会调用这个方法,通过执行 “list.Add(this)” 将自己加入到 list 列表中保存起来。
源码:
// 当走过 Start 后,每次调用 OnEnable 都会调用这个 函数
protected override void OnInit ()
{
if (list.Contains(this)) return;
base.OnInit();
//查找自己父节点中有没有UIPanel,有就设置为父节点,没有就设置为null
FindParent();
// Apparently having a rigidbody helps
#if UNITY_4_3 || UNITY_4_5 || UNITY_4_6 || UNITY_4_7
if (rigidbody == null && mParentPanel == null)
#else
if (GetComponent<Rigidbody>() == null && mParentPanel == null)
#endif
{
UICamera uic = (anchorCamera != null) ? mCam.GetComponent<UICamera>() : null;
if (uic != null)
{
if (uic.eventType == UICamera.EventType.UI_3D || uic.eventType == UICamera.EventType.World_3D)
{
Rigidbody rb = gameObject.AddComponent<Rigidbody>();
rb.isKinematic = true;
rb.useGravity = false;
}
// It's unclear if this helps 2D physics or not, so leaving it disabled for now.
// Note that when enabling this, the 'if (rigidbody == null)' statement above should be adjusted as well.
//else
//{
// Rigidbody2D rb = gameObject.AddComponent<Rigidbody2D>();
// rb.isKinematic = true;
//}
}
}
//重建 drawcall,每次重新激活 UIPanel 都会重建所有的 drawcall
mRebuild = true;
mAlphaFrameID = -1;
mMatrixFrame = -1;
//每次创建一个新的uipanel,都会被添加到这个静态数组中,并且进行所有的 panel 排序
list.Add(this);
//panel 按照 mDepth 深度按照升序排列
list.Sort(CompareFunc);
}
protected override void OnEnable ()
{
mRebuild = true; //每次重新激活这个对象后,会重置 drawcall
mAlphaFrameID = -1;
mMatrixFrame = -1;
OnStart();
base.OnEnable();
mMatrixFrame = -1;
}
UIPanel 更新
这里面是整个 UIPanel 创建和调用 UIDrawcall 的核心代码,我们一个一个分析:
LateUpdate() : 每一帧调用 list 更新所有正在使用的 UIPanel, 做两件事:
UpdateSelf 更新 panel,所有它的所有 uiwidget ,设置 drawcall 相关信息
UpdateDrawCalls 设置渲染循序
源码:
void LateUpdate ()
{
#if UNITY_EDITOR && !UNITY_5_5_OR_NEWER
if (mUpdateFrame != Time.frameCount || !Application.isPlaying)
#else
if (mUpdateFrame != Time.frameCount)
#endif
{
mUpdateFrame = Time.frameCount;
// Update each panel in order
//调用每个panel 的 UpdateSelf 方法,更新自己信息
for (int i = 0, imax = list.Count; i < imax; ++i)
list[i].UpdateSelf();
int rq = 3000;
// Update all draw calls, making them draw in the right order
// 更新所有 panel 中的 drawcall 绘制调用,使它们以正确的顺序绘制,ui的起点都是 3000
// 主要设置渲染顺序
for (int i = 0, imax = list.Count; i < imax; ++i)
{
UIPanel p = list[i];
if (p.renderQueue == RenderQueue.Automatic)
{
p.startingRenderQueue = rq;
p.UpdateDrawCalls(i);
rq += p.drawCalls.Count;
}
else if (p.renderQueue == RenderQueue.StartAt)
{
p.UpdateDrawCalls(i);
if (p.drawCalls.Count != 0)
rq = Mathf.Max(rq, p.startingRenderQueue + p.drawCalls.Count);
}
else // Explicit
{
p.UpdateDrawCalls(i);
if (p.drawCalls.Count != 0)
rq = Mathf.Max(rq, p.startingRenderQueue + 1);
}
}
}
}
/// 更新 panel,所有它的所有 uiwidget 和 drawcall
void UpdateSelf ()
{
// 每一帧需要,判断物体是否是一个静态物体,如果是,这个就是false,如果是运动的,为true
mHasMoved = cachedTransform.hasChanged;
//更新矩阵变化
UpdateTransformMatrix();
//更新所有layer
UpdateLayers();
//更新所有的widget
UpdateWidgets();
//是否重建,重建是重建所有panel下的widget的drawcall
if (mRebuild)
{
//重建所有的DrawCall (这里就是性能优化重点!减少不必要的重建)
mRebuild = false;
FillAllDrawCalls();
}
else
{
//判断哪个drawcall 被污染了,就需要重新去调用这个drawcall绘制图像
for (int i = 0; i < drawCalls.Count; )
{
UIDrawCall dc = drawCalls[i];
//如果这个 drawcall isDirty 为true,就需要调用FillDrawCall调用当前drawcall去重绘图形
if (dc.isDirty && !FillDrawCall(dc))
{
UIDrawCall.Destroy(dc);
drawCalls.RemoveAt(i);
continue;
}
++i;
}
}
//每添加一个 uiwidget 就要更新一次
if (mUpdateScroll)
{
mUpdateScroll = false;
UIScrollView sv = GetComponent<UIScrollView>();
if (sv != null) sv.UpdateScrollbars();
}
if (mHasMoved)
{
mHasMoved = false;
mTrans.hasChanged = false;
}
}
UpdateTransformMatrix():更新矩阵,这个主要和裁剪相关,需要计算一下裁剪的区域,在看源码前先看下它的设计原理
:
再来看源码就全部理解了:
void UpdateTransformMatrix ()
{
int fc = Time.frameCount;
//每一帧或者运动的物体要更新
if (mHasMoved || mMatrixFrame != fc)
{
mMatrixFrame = fc;
//将当前挂载这个组件的点,从世界坐标转化成局部坐标的矩阵
worldToLocal = cachedTransform.worldToLocalMatrix;
//* 0.5 是为了计算显示区域的一半宽度,一般高度
Vector2 size = GetViewSize() * 0.5f;
//计算裁剪区域的中心坐标
float x = mClipOffset.x + mClipRange.x;
float y = mClipOffset.y + mClipRange.y;
mMin.x = x - size.x; //计算左边中心点的坐标
mMin.y = y - size.y; //计算下边中心点的坐标
mMax.x = x + size.x; //计算右边中心点的坐标
mMax.y = y + size.y; //计算上边中心点的坐标
}
}
UpdateLayers():如果panel 的 layer 发生变化就要更新所有的 wideget 的layers
源码:
void UpdateLayers ()
{
// Always move widgets to the panel's layer
if (mLayer != cachedGameObject.layer)
{
mLayer = mGo.layer;
for (int i = 0, imax = widgets.Count; i < imax; ++i)
{
UIWidget w = widgets[i];
//保证 widget 的 layer 永远 == panel 的 layer
if (w && w.parent == this) w.gameObject.layer = mLayer;
}
ResetAnchors();
//更新所有 drawcall 的layer
for (int i = 0; i < drawCalls.Count; ++i)
drawCalls[i].gameObject.layer = mLayer;
}
}
UpdateWidgets():更新所有属于这个 panel 中的 widget 相关信息
源码:
void UpdateWidgets()
{
bool changed = false;
bool forceVisible = false;
bool clipped = hasCumulativeClipping;
if (!cullWhileDragging)
{
//对于scrolliew中在拖动时不触发选中 widget 的情况下,走这里
for (int i = 0; i < UIScrollView.list.size; ++i)
{
UIScrollView sv = UIScrollView.list[i];
if (sv.panel == this && sv.isDragging) forceVisible = true;
}
}
if (mForced != forceVisible)
{
mForced = forceVisible;
mResized = true;
}
// Update all widgets
//更新所有的 widgets
int frame = Time.frameCount;
for (int i = 0, imax = widgets.Count; i < imax; ++i)
{
UIWidget w = widgets[i];
// If the widget is visible, update it
if (w.panel == this && w.enabled)
{
#if UNITY_EDITOR
// When an object is dragged from Project view to Scene view, its Z is...
// odd, to say the least. Force it if possible.
// 当一个对象从 Project 视图拖动到场景视图时,它的Z是旧的,在编辑器模式下,要做一些调整
if (!Application.isPlaying)
{
Transform t = w.cachedTransform;
if (t.hideFlags != HideFlags.HideInHierarchy)
{
t = (t.parent != null && t.parent.hideFlags == HideFlags.HideInHierarchy) ?
t.parent : null;
}
if (t != null)
{
for (; ; )
{
if (t.parent == null) break;
if (t.parent.hideFlags == HideFlags.HideInHierarchy) t = t.parent;
else break;
}
if (t != null)
{
Vector3 pos = t.localPosition;
pos.x = Mathf.Round(pos.x);
pos.y = Mathf.Round(pos.y);
pos.z = 0f;
if (Vector3.SqrMagnitude(t.localPosition - pos) > 0.0001f)
t.localPosition = pos;
}
}
}
#endif
// First update the widget's transform
if (w.UpdateTransform(frame) || mResized || (mHasMoved && !alwaysOnScreen))
{
// Only proceed to checking the widget's visibility if it actually moved
bool vis = forceVisible || (w.CalculateCumulativeAlpha(frame) > 0.001f);
w.UpdateVisibility(vis, forceVisible || alwaysOnScreen || ((clipped || w.hideIfOffScreen) ? IsVisible(w) : true));
}
// Update the widget's geometry if necessary
//更新这个 widget 里面的几何信息
if (w.UpdateGeometry(frame))
{
changed = true;
if (!mRebuild)
{
// Find an existing draw call, if possible
//在widget 变化后,检查这个widget 是否有 drawcall ,如果有就置 isDirty 为true,如果没有就要调用 FindDrawCall 去找一个drawcall
if (w.drawCall != null) w.drawCall.isDirty = true;
else FindDrawCall(w);
}
}
}
}
// Inform the changed event listeners
if (changed && onGeometryUpdated != null) onGeometryUpdated();
mResized = false;
}
FillAllDrawCalls():重建所有 drawcall
,这个就是最重要的部分,整个创建 UIDrawcall 的核心代码,性能优化之 drawcall 相关的部分主要就是这部分内容。
核心原理是:先给所有的 UIWidget 组件排序,按照深度,材质,等方式排好顺序后,开始创建 drawcall,如果材质,贴图,shader 相同的相邻 UIWidget 会使用同一个 drawcall ,否则就会重新创建一个新的 drawcall 加入到列表中。
这里先看一下 UIWidget 的排序规则,这段代码写在 “UIWidget” 类中,代码如下:
//1.比较层级,从小到大
//2.层级相同,比较材质,有材质的在前面
//3.层级相同,都有材质,并且材质不同,比较预制的id,从小到大
//因为:list 比较大小,按照升序来排列。0表示相等,-1表示小于,1表示大于
//所以:如果 left > right reutrn 1, 表示 left 和 right 要交换,就是按照从小到大的顺序排列
// 如果 left > right return -1, 表示 left 和 right 不交换,那么就是按照从大到小的顺序排列
[System.Diagnostics.DebuggerHidden]
[System.Diagnostics.DebuggerStepThrough]
static public int PanelCompareFunc (UIWidget left, UIWidget right)
{
if (left.mDepth < right.mDepth) return -1;
if (left.mDepth > right.mDepth) return 1;
Material leftMat = left.material;
Material rightMat = right.material;
if (leftMat == rightMat) return 0;
if (leftMat == null) return 1;
if (rightMat == null) return -1;
return (leftMat.GetInstanceID() < rightMat.GetInstanceID()) ? -1 : 1;
}
创建 drawcall 源码:
void FillAllDrawCalls ()
{
//将上一次的所有 drawcall 信息删除
for (int i = 0; i < drawCalls.Count; ++i)
UIDrawCall.Destroy(drawCalls[i]);
drawCalls.Clear();
Material mat = null; //缓存上一个 材质
Texture tex = null; //缓存上一个 贴图
Shader sdr = null; //缓存上一个 shader
UIDrawCall dc = null; //缓存上一个 drawcall
int count = 0;
//给所有 widget 排序
if (mSortWidgets) SortWidgets();
//注意这里是排好序的widget
for (int i = 0; i < widgets.Count; ++i)
{
UIWidget w = widgets[i];
//只重建激活的并且有顶点的 uiwidget,比如创建一个空节点,只挂在一个uiwidget 组件,并不会重建 drawcall
if (w.isVisible && w.hasVertices)
{
//这里是创建整个drawcall的核心代码
Material mt = w.material;
if (onCreateMaterial != null) mt = onCreateMaterial(w, mt);
Texture tx = w.mainTexture;
Shader sd = w.shader;
//如果材质 ,贴图,shader 任意一个和上一个不同,就要将旧的dc添加进列表,并且创建一个新的drawcall
//注意这里是比较连续的 widget
if (mat != mt || tex != tx || sdr != sd)
{
//这里需要把上一个drawcall 添加进来,然后将 dc 置为null,方便下面创建一个新的dc
if (dc != null && dc.verts.Count != 0)
{
drawCalls.Add(dc);
dc.UpdateGeometry(count);
dc.onRender = mOnRender;
mOnRender = null;
count = 0;
dc = null;
}
mat = mt;
tex = tx;
sdr = sd;
}
if (mat != null || sdr != null || tex != null)
{
//这里判断是否需要创建一个新的drawcall
if (dc == null)
{
dc = UIDrawCall.Create(this, mat, tex, sdr);
dc.depthStart = w.depth;
dc.depthEnd = dc.depthStart;
dc.panel = this;
dc.onCreateDrawCall = onCreateDrawCall;
}
else
{
//如果多个widget 共用同一个drawcall 那么需要更新这个drawcall depth 的范围
int rd = w.depth;
if (rd < dc.depthStart) dc.depthStart = rd;
if (rd > dc.depthEnd) dc.depthEnd = rd;
}
//设置 widget 的drawcall
w.drawCall = dc;
++count;
//将顶点,uv,颜色,法线等信息放入缓冲区
if (generateNormals) w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, dc.norms, dc.tans, generateUV2 ? dc.uv2 : null);
else w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, null, null, generateUV2 ? dc.uv2 : null);
if (w.mOnRender != null)
{
if (mOnRender == null) mOnRender = w.mOnRender;
else mOnRender += w.mOnRender;
}
}
}
else w.drawCall = null;
}
//将最后一个创建的 drawcall 添加进来
if (dc != null && dc.verts.Count != 0)
{
drawCalls.Add(dc);
dc.UpdateGeometry(count);
dc.onRender = mOnRender;
mOnRender = null;
}
}
这里需要补充一个知识点:WriteToBuffers()
看了源码我们应该都知道,UIDrawcall 中开始准备绘制的核心代码接口就是 “UpdateGeometry” (核心方法,通过顶点,uv,颜色,贴图等信息绘制ui图形),那么这个方法中所需要的 顶点,UV,颜色等信息都是从哪里来的呢?
没错,就是从 “WriteToBuffers” 这个方法中来的。
“WriteToBuffers” 方法放在 “UIGeometry” 类中,这个类是一个辅助类,主要用于临时缓存一些几何信息,每次绘制 UI 时,都会将这些需要绘制的顶点等相关信息,通过这个接口设置到 UIDrawcall 的缓冲区中,然后调用 “UpdateGeometry” 设置,设置完成后,缓冲区又会将这些数据清楚掉,供下一次调用。
其他一些重要源码如下:
/// 为指定的绘制调用填充几何图形。
public bool FillDrawCall (UIDrawCall dc)
{
if (dc != null)
{
dc.isDirty = false;
int count = 0; // 记录需要刷新的widget的个数
//遍历所有 widget,寻找和dc 调用相同的 drawcall
for (int i = 0; i < widgets.Count; )
{
UIWidget w = widgets[i];
if (w == null)
{
#if UNITY_EDITOR
Debug.LogError("This should never happen");
#endif
widgets.RemoveAt(i);
continue;
}
if (w.drawCall == dc)
{
if (w.isVisible && w.hasVertices)
{
++count;
//重新将顶点,uv,颜色,法线等信息放入缓冲区
if (generateNormals) w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, dc.norms, dc.tans, generateUV2 ? dc.uv2 : null);
else w.WriteToBuffers(dc.verts, dc.uvs, dc.cols, null, null, generateUV2 ? dc.uv2 : null);
if (w.mOnRender != null)
{
if (mOnRender == null) mOnRender = w.mOnRender;
else mOnRender += w.mOnRender;
}
}
//如果 widget 被 false 或者没有顶点信息后,将 dc 置为null
else w.drawCall = null;
}
++i;
}
if (dc.verts.Count != 0)
{
//绘制ui图形
dc.UpdateGeometry(count);
dc.onRender = mOnRender;
mOnRender = null;
return true;
}
}
return false;
}
//查找drawcall,如果一个 widget 没有drawcall 就要调用这个给它找一个,如果找不到,就要重建整个drawcall
public UIDrawCall FindDrawCall (UIWidget w)
{
var mat = w.material;
var tex = w.mainTexture;
var shader = w.shader;
var depth = w.depth;
for (int i = 0; i < drawCalls.Count; ++i)
{
UIDrawCall dc = drawCalls[i];
int dcStart = (i == 0) ? int.MinValue : drawCalls[i - 1].depthEnd + 1;
int dcEnd = (i + 1 == drawCalls.Count) ? int.MaxValue : drawCalls[i + 1].depthStart - 1;
//从这里可以看出,widget 的深度,材质,shader, texture 都会影响到是否需要重建
if (dcStart <= depth && dcEnd >= depth)
{
if (dc.baseMaterial == mat && dc.shader == shader && dc.mainTexture == tex)
{
if (w.isVisible)
{
w.drawCall = dc;
if (w.hasVertices) dc.isDirty = true;
return dc;
}
}
else mRebuild = true;
return null;
}
}
mRebuild = true;
return null;
}
//添加一个 widget
public void AddWidget (UIWidget w)
{
mUpdateScroll = true;
if (widgets.Count == 0)
{
widgets.Add(w);
}
else if (mSortWidgets)
{
widgets.Add(w);
SortWidgets();
}
else if (UIWidget.PanelCompareFunc(w, widgets[0]) == -1)
{
widgets.Insert(0, w);
}
else
{
for (int i = widgets.Count; i > 0; )
{
if (UIWidget.PanelCompareFunc(w, widgets[--i]) == -1) continue;
widgets.Insert(i+1, w);
break;
}
}
FindDrawCall(w);
}
//删除一个 widget
public void RemoveWidget (UIWidget w)
{
if (widgets.Remove(w) && w.drawCall != null)
{
int depth = w.depth;
//删除 widget 时,depth 会影响是否会被 重建全部 drawcall
if (depth == w.drawCall.depthStart || depth == w.drawCall.depthEnd)
mRebuild = true;
w.drawCall.isDirty = true;
w.drawCall = null;
}
}
UIPanel 销毁
OnDisable():每次禁用(false) 当前 UIPanel 对象时调用,通过执行 “list.Remove(this)” 将自己从 list 列表中删除,并且清空相关 UIDrawcall 列表。
源码:
// false 掉当前节点对象
protected override void OnDisable ()
{
//如果对象被 false 就会清除掉所有的 drawcall,等到下次对象被激活的时候在重建 drawcall
for (int i = 0, imax = drawCalls.Count; i < imax; ++i)
{
UIDrawCall dc = drawCalls[i];
if (dc != null) UIDrawCall.Destroy(dc);
}
drawCalls.Clear();
//删除自己的 uipanel
list.Remove(this);
mAlphaFrameID = -1;
mMatrixFrame = -1;
if (list.Count == 0)
{
UIDrawCall.ReleaseAll();
mUpdateFrame = -1;
}
base.OnDisable();
}
结论
1.回答之前的思考题:
-
谁创建的 UIDrawcall,什么时候创建?
答:UIPanel 创建的;
每次在重建所有 UIDrawcall 的时候创建的,在 FillAllDrawCalls 方法中重建所有 Drawcall。 -
谁调用的 UIDrawcall,什么时候调用?
答:UIPanel 调用的;
UIPanel 通过调用 UIDrawcall 的 UpdateGeometry 方法,去重绘 UI 信息,主要在两种情况中调用:当重建所有的 UIDrawcall 的时候调用;UpdateSelf 中如果某个 Drawcall 的 isDirty 被设置为 true 时,就需要去检查是否需要重绘,如果需要,会调用 UpdateGeometry ; -
谁移除的,什么时候移除?
答:UIPanel 移除的;
每次重建所有的 Drawcall 之前会先将原来的清除掉;
禁用 当前 UIPanel 组件的时候,调用 OnDisable 中会清除当前 UIPanel 中创建的所有 Drawcall ;
在 UpdateSelf 中更新的时候,如果某个 Drawcall 中 isDirty 为 true 时,并且没有需要重绘的信息,就将其移除。
2.思考:
思考:如何优化 UIDrawcall 的调用次数,减少 drawcall,优化性能?