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

By D.S.Qiu

尊重他人的劳动,支持原创,转载请注明出处:http.dsqiu.iteye.com

之前项目中用的NGUI的版本是3.0.7 f3,开始的时候感觉没有什么问题,直达最近项目UI的完成度比较高时,就突然出现掉帧很严重的现象,即使只有一个UI打开(其他都是active = false的情况下),打开profier,发现UIPanel LateUpdate 竟然占了CPU使用率的50%左右,这太恐怖了,虽然之前看到过有吐槽NGUI的机制的,但是我觉得为了保证通用牺牲一些性能还是在所难免的,但是没想到这个版本竟然这么废。

之前虽然研究过NGUI的UIWidget, UIDrawCall,UIGeometry和 UIPanel等基础脚本(NGUI所见即所得之UIWidget , UIGeometry & UIDrawCall,NGUI所见即所得之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. {
  3. get{return mMaterial;}
  4. set
  5. {
  6. if (mMaterial != value)
  7. {
  8. mMaterial = value;
  9. mRebuildMat = true;
  10. }
  11. }
  12. }
  13. public Shader shader
  14. {
  15. get{ return mShader;}
  16. set
  17. {
  18. if (mShader != value)
  19. {
  20. mShader = value;
  21. mRebuildMat = true;
  22. }
  23. }
  24. }

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. void OnDisable ()
  3. {
  4. depthStart = int.MaxValue;
  5. depthEnd = int.MinValue;
  6. panel = null;
  7. manager = null;
  8. mMaterial = null;
  9. mTexture = null;
  10. NGUITools.DestroyImmediate(mDynamicMat);
  11. mDynamicMat = null;
  12. }
  13. void OnDestroy ()
  14. {
  15. NGUITools.DestroyImmediate(mMesh);
  16. }

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. }
  17. static public void Destroy (UIDrawCall dc)
  18. {
  19. if (dc)
  20. {
  21. if (Application.isPlaying)
  22. {
  23. if (mActiveList.Remove(dc))
  24. {
  25. NGUITools.SetActive(dc.gameObject, false);
  26. mInactiveList.Add(dc);
  27. }
  28. }
  29. else
  30. {
  31. mActiveList.Remove(dc);
  32. NGUITools.DestroyImmediate(dc.gameObject);
  33. }
  34. }
  35. }

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变得更少

由于时间关系(马上2:30了),就只能写到这里,如果你有NGUI的任何问题,欢迎和D.S.Qiu进行交流讨论。

如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。

转载请在文首注明出处:http://dsqiu.iteye.com/blog/2025177

更多精彩请关注D.S.Qiu的博客和微博(ID:静水逐风)

转载于:https://www.cnblogs.com/123ing/p/3704964.html

NGUI所见即所得之深入剖析UIPanel,UIWidget,UIDrawCall底层原理相关推荐

  1. Go语言底层原理剖析

    作者:郑建勋 出版社:电子工业出版社 品牌:博文视点 出版时间:2021-08-01 Go语言底层原理剖析

  2. 深入剖析ASP.NET的编译原理之一:动态编译(Dynamical Compilation)

    原文:http://www.cnblogs.com/artech/archive/2007/05/21/753620.html Microsoft 的Visual Studio为我们在应用开发中提供的 ...

  3. 深入理解Go底层原理剖析 (送书)

    互联网迅猛发展的数十年时间里,不断面领着各种新的场景与挑战,例如大数据.大规模集群计算.更复杂的网络环境.多核处理器引起对于高并发的需求,云计算,上千万行的服务器代码-- 那些成熟但上了年纪的语言没能 ...

  4. 『Go 语言底层原理剖析』文末送书

    互联网迅猛发展的数十年时间里,不断面领着各种新的场景与挑战,例如大数据.大规模集群计算.更复杂的网络环境.多核处理器引起对于高并发的需求,云计算,上千万行的服务器代码-- 那些成熟但上了年纪的语言没能 ...

  5. 深入剖析ASP.NET的编译原理之二:预编译(Precompilation)

    (转载)在本篇文章的第一部分:[原创]深入剖析ASP.NET的编译原理之一:动态编译(Dynamical Compilation),详细讨论了ASP.NET如何进行动态编译的,现在我们来谈谈另外一种重 ...

  6. 万字长文剖析清楚 Go 语言 defer 原理

    大纲 编译器怎么编译 defer `struct _defer` 数据结构 `struct _defer` 内存分配 执行 defer 函数链( `deferreturn`  ) defer 怎么传递 ...

  7. (转)编码剖析@Resource注解的实现原理

    http://blog.csdn.net/yerenyuan_pku/article/details/52860046 上文我们已经学会使用@Resource注解注入属性.学是学会了,但也仅限于会使用 ...

  8. (转)编码剖析Spring装配基本属性的原理

    http://blog.csdn.net/yerenyuan_pku/article/details/52856465 上回我们已经讲到了Spring依赖注入的第一种方式,现在我们来详解第二种方式,须 ...

  9. (转)编码剖析Spring依赖注入的原理

    http://blog.csdn.net/yerenyuan_pku/article/details/52834561 Spring的依赖注入 前面我们就已经讲过所谓依赖注入就是指:在运行期,由外部容 ...

最新文章

  1. jieba分词_自然语言NLP必备(1),jieba工具 5 行代码对整篇文章分词
  2. java Cache框架
  3. python安装包-安装 Python 模块
  4. 十大经典排序算法动画与解析,看我就够了!(配代码完全版)
  5. 反编译工具Reflector ILSpy
  6. o3命令 linux,linux文本处理命令 一
  7. 基于Python的指数基金量化投资 - 指数投资技巧(二)定期不定额
  8. PMP考试备考指南基础知识
  9. 广告违规词、敏感词在线检测
  10. 2022全球程序员薪资排行:中国倒数,美国写Go最挣钱
  11. aws linux使用ssh登陆_aws 创建新用户并使用 ssh 登录
  12. Python数据分析与挖掘实战第三章笔记之贡献度分析代码
  13. 爽一把手写Bundle Adjustment
  14. [转] PHP开发通用型标题图片功能
  15. 【光电智造】机器人视觉伺服技术
  16. 吃猕猴桃,当然不不不不能错过贵州的啦
  17. json rpgmv 加密_RPGMakerMV探秘01-文件结构
  18. 《数据结构与算法分析》课程设计——贪吃蛇问题
  19. nowcoder:[编程题] 头条校招(贪心)
  20. uml各类图--完整全面实例

热门文章

  1. 二、Netty服务端/客户端启动整体流程
  2. 操作系统(二)操作系统的四个特征
  3. Hi3516A开发-- 板卡串口烧写
  4. 以太坊Oracle系列二:My Oracle
  5. android linker 浅析
  6. 彻底弄懂dalvik字节码【二】
  7. Android实战】DroidPlugin插件化应用分析
  8. blob转file对象_JavaScript Blob 对象解析
  9. ubuntu 20.04 设置网关_如何把Ubuntu升级到Ubuntu 20.04 LTS 最新版本
  10. UCINET 社会网络分析工具