[Unity] - NGUI所见即所得之深入剖析UIPanel,UIWidget,UIDrawCall底层原理

之前项目中用的NGUI的版本是3.0.7 f3,开始的时候感觉没有什么问题,直达最近项目UI的完成度比较高时,就突然出现掉帧很严重的现象,即使只有一个UI打开(其他都是active = false的情况下),打开profier,发现UIPanel LateUpdate 竟然占了CPU使用率的50%左右,这太恐怖了,虽然之前看到过有吐槽NGUI的机制的,但是我觉得为了保证通用牺牲一些性能还是在所难免的,但是没想到这个版本竟然这么废。
        之前虽然研究过NGUI的UIWidget, UIDrawCall,UIGeometry和 UIPanel等基础脚本( NGUI所见即所得之UIWidget , UIGeometry & UIDrawCallNGUI所见即所得之UIPanel),也大概清楚了NGUI的绘制原理。但对具体的逻辑还是不够清楚,有点凤毛麟角。为了更好的改进NGUI的性能以及更加规范使用NGUI,只有把NGUI的底层吃透。
        由于在之前的文章介绍了UIGeometry,UIDrawCall和UIWidget之间的关系,以及UIPanel的管理机制,所以本文主要剖析底层的原理,主要要弄清楚一下问题:

               1. transform ,大小(size)的变化的底层绘制影响
               2.颜色(包括透明度)变化的底层绘制影响
               3.enable 和 disable 状态变化底层的处理
               4.UIDrawCall 和 UIPanel 机制的细节
      
        未免读者理不顺,先简单说下UIGeometry,UIDrawCall和UIWidget的关系:UIWidget是UI的基础组件(UILabel,UISprite)的基类,含有组件的基本信息(width,Height,color等),UIGeometry是UIWidget的几何数据,记录了顶点坐标,贴图的UVs和颜色等信息,UIDrawCall是将多个UIWidget的UIGeometry组合起来一起绘制,具体的UIWidget如果共用一个UIDrawCall由UIPanel控制,要想了解更多可以点击上面的链接的文章查看。
        虽然从人的求知欲角度,我们的疑问是按照上面 1-4 排列的,但是下面却是从 4开始介绍,只要把4理解透了3,2,1就自然迎刃而解了。
UIDrawCall
        UIGeometry相对简单,这里就不再浪费篇幅介绍了,UIDrawCall是绘制的基础组件,还是有必要仔细介绍下。
1.成员变量
        仅对几个比较重要又搞不明白的变量进行解析:
        a)List<UIDrawCall> mActiveList 和 mInactiveList : 为什么会有两个List,mAcitveList 保持当前激活的UIDrawCall, mInactiveList主要是用于回收UIDrawCall.Destroy()的UIDrawCall,以达到循环利用避免内存的反复申请和释放,减少GC的次数。这个机制前面介绍的 vp_Timer采用这个策略。
        b)Material mMaterial 和 mDynamicMat:不是讲究节约内存么,怎么会有两个Material,mMaterial就是我们图集的材质Material,mDynamicMat是实际采用的Material,因为UIPanel 的 Clipping有 AlphaClipp 和 SoftClip 这两个是要通过切换Shader来实现的,所以需要对应动态创建一个Material,这个就是mDynamicMat的存在。
        c)bool mRebuildMat 和 isDirty:这两者表示UIDrawCall所处的状态,当改变UIDrawCall的 Material 和 Shader ,mRebuildMat就变为 true,就会引起 RebuildMaterial()的调用。isDirty若为 true ,表示UIDrawCall要进行重写“填充”,调用Set函数
C#代码  收藏代码
  1. public Material baseMaterial 
  2.     get{return mMaterial;} 
  3.     set 
  4.     { 
  5.         if (mMaterial != value) 
  6.         { 
  7.             mMaterial = value; 
  8.             mRebuildMat = true
  9.         } 
  10.     } 
  11. public Shader shader 
  12.     get{ return mShader;} 
  13.     set 
  14.     { 
  15.         if (mShader != value) 
  16.         { 
  17.             mShader = value; 
  18.             mRebuildMat = true
  19.         } 
  20.     } 
2.几个重要的函数
        a)CreateMaterial, RebuildMaterial 和 UpdateMaterial,这是三个后面包含前面,总之就是完成材质的创建或更新。
        b)Set (BetterList<Vector3> verts,BetterList<Vector3> norms,BetterList<Vector4> tans,BetterList<Vector2> uvs,BetterList<Color32> cols),根据verts,norms,tans,uvs,cols重新构建Mesh,MeshRender
C#代码  收藏代码
  1. mMesh.vertices = verts.buffer; 
  2. mMesh.uv = uvs.buffer; 
  3. mMesh.colors32 = cols.buffer; 
  4. if (norms != null) mMesh.normals = norms.buffer; 
  5. if (tans != null) mMesh.tangents = tans.buffer; 
      c)OnEnable,Ondisable 和 OnDestroy:销毁了mDynamicMat,可以看出Material比Mesh更简单,不用太考虑内存问题,然后OnDestroy()没有发现调用。
C#代码  收藏代码
  1. void OnEnable () { mRebuildMat = true; } 
  2.  
  3. void OnDisable () 
  4.     depthStart = int.MaxValue; 
  5.     depthEnd = int.MinValue; 
  6.     panel = null
  7.     manager = null
  8.     mMaterial = null
  9.     mTexture = null
  10.  
  11.     NGUITools.DestroyImmediate(mDynamicMat); 
  12.     mDynamicMat = null
  13.  
  14. void OnDestroy () 
  15.     NGUITools.DestroyImmediate(mMesh); 
       d)Create , Clear 和 Destroy:Create 先从mInactiveList中取出一个,在附上属性达到重复利用,Destroy是将没用的UIDrawCall从mActiveList移到mInactiveList中:
C#代码  收藏代码
  1. static UIDrawCall Create (string name) 
  2. {        
  3.                //省略其他处理 
  4.     if (mInactiveList.size > 0) 
  5.     { 
  6.         UIDrawCall dc = mInactiveList.Pop(); 
  7.         mActiveList.Add(dc); 
  8.         if (name != null) dc.name = name; 
  9.         NGUITools.SetActive(dc.gameObject, true); 
  10.         return dc; 
  11.     } 
  12.                //省略其他处理 
  13.     // Create the draw call 
  14.     mActiveList.Add(newDC); 
  15.     return newDC; 
  16. static public void Destroy (UIDrawCall dc) 
  17.     if (dc) 
  18.     { 
  19.         if (Application.isPlaying) 
  20.         { 
  21.             if (mActiveList.Remove(dc)) 
  22.             { 
  23.                 NGUITools.SetActive(dc.gameObject, false); 
  24.                 mInactiveList.Add(dc); 
  25.             } 
  26.         } 
  27.         else 
  28.         { 
  29.             mActiveList.Remove(dc); 
  30.             NGUITools.DestroyImmediate(dc.gameObject); 
  31.         } 
  32.     } 

UIPanel
       之前就介绍过UIPanel,也画了UIPanel主要函数的调用栈( 点击查看),这里也简单罗列下LateUpdate的函数调用:
LateUpdate
      UpdateSelf
                UpdateTransformMatrix : 调整 worldToLocal 矩阵用于调整其管理的UIWidget的transform,并进一步调整顶点信息,还调整clipOffset的变量
                UpdateLayers : 更新LayerMask
                UpdateWidgets : 调整UIWidget
                            UIWidget.UpdateGeometry : 调整UIWidget的几何(顶点等)信息
                                              OnFill(geometry.verts, geometry.uvs, geometry.cols): 如果颜色(透明度)和大小等改变就重新填充顶点信息
                                              geometry.ApplyTransform : transform发生改变,调整UIGeometry中顶点的位置(矩阵计算)
                FillAllDrawCalls  or FillDrawCall : 重新构建所有UIDrawCall (当UIWdiget的depth发生变化),否则只调整有UIWidget的UIDrawCall
      UpdateDrawCalls : 调整UIPanel管理的UIDrawCall 的 transform 和 clip 等属性
      越来越觉得NGUI的代码组件结构越来越清晰,虽然篇幅很长(有1600多行)但理解还是可以很简单的。

UIWidget
       UIWidget有一个变量 mChange 和一个函数 MarkAsChange() 很重要,这两个标记UIWidget是否变化需要进行调整的状态。
                1.当 Anchor , Pivot , Alpha 以及 UILabel 和 UISprite 的一些状态的改变 mChange = true ,即会调整Geometry信息
                2.MarkAsChange 会执行 drawCall.isDirty = true; 这样就会导致其所属的 UIDrawCall 需要重写构建

针对前面 1-3 的疑问进行如下总结:
      UIWidget(UILabel , UISprite)的任何变化(transform , drawSize , width , heigth , color , pivot ,anchor 等)变化都会引起绘制该UIWidget进行重新构建——对Mesh的顶点进行刷新,尤其是depth的变化会使得所有UIDrawCall 进行重写调整,这是非常耗性能的。
      
总结:
       NGUI的好处就是:合并Mesh和图集节省DrawCall,由于影响Mesh的因素太多了,所以会“牵一发而动全身”,NGUI采取的一个通用的策略,没有对不同的情况做不同的处理,都是采用某个UIDrawCall全部刷新甚至是全部UIDrawCall的刷新,这也是大家吐槽的“重中之重”。
       D.S.Qiu认为针对不用的情况还是会有不少优化的,比如改变alpha值,可以不需要重新调整顶点verts,而只需要单独调整cols的alpha通道,改变depth也不需要全部调整UIDrawCall,这样明显是没有做到严格的管理的。
       对此,D.S.Qiu提出2点使用NGUI制作UI的建议:
                1)尽量是UIWidget静动分离,即静止的尽量合成单独一个UIPanel,会变化的就放在另外一个UIPanel
                2)尽量控制UIPanel和UIDrawCall的数量,充分利用图集的空间,对“夹层”的情况可以通过图集的调整,使得UIDrawCall变得更少
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值