【性能】ugui对象不可视

前言

在很早之前我做过一次性能分析,分析物体在各种不可视方案下的性能开销但是这个是在对于Gameobject的分析,现在在做ugui的界面显隐的时候,又多出了好几种方案,现在来最这几种方案做一个分析。

正文

测试代码还是和以前一样,对物体进行10万次显隐操作。

隐藏控件

Canvas

在ugui中,界面的渲染都是基于canvas的,所以当我们隐藏掉canvas这个控件的时候,界面也将看不见,所以我们先来做一次canvas的控件显隐操作

因为我的子物体有6个image所以当canvas被隐藏的时候,它会去回调image的渲染回调,所以我们可以看到调用image.OnCanvasHierarchyChanged调用了120万次。回调中因为会用到一些对象,所以它的GC压力很大。

Image

对于单一的物体,我们以Image为例,它的渲染是基于它的Image这个控件,所以当我们把控件隐藏了,它将不再被渲染

这里我们看到它的主要开销是OnEnable和OnDisable这两个方法。

设置透明

CanvasGroup

CanvasGroup是用来控制子控件的alpha等一些界面属性的,所以当我们把它的alpha设置成0的时候,子节点的alpha的值也就变成0了。

看到ugui放出来的CanvasGroup的代码可以看到它有一部分的实现在c++里的,当CanvasGroup的alpha的值发生变化,应该是要发送消息告诉子节点的透明度的变化,所以这里的120万的调用我们猜测是因为子节点有六个,所以要抛消息到所有的子节点上。

image

当image的alpha的值被设置成0的时候,我们就看不到这个组件了

可以看到它就只需调用自身,没有其他的函数调用。

设置大小

这个就在之前的测试里做过的,因为大小为0的时候,它将不会被渲染出来,所以也就不会可视化出来。改变的是矩阵。

结论

在我看来我觉得还是设置大小是最优的显隐方案,用代码测试一次就好了。大家一起来看看还有什么情况是没有测试到的呢。

【unity】优化-cpu

CPU耗时

类中存在空的updata,lateupdate和FixedUpdate这样的方法

出现问题的主要原因

  1. 这些方法是native层对托管层的调用,c++与c#之间的通信本身就存在一定的开销
  2. 当这些方法的调用,Unity会进行一系列的安全监测(比如保证GameObject没有被销毁)导致cpu时间的消耗

这个类中的方法存在Camera.main的调用

这个方法获取属性的方法实际上是一个Get的方法,每次调用的时候都会去寻找场景中第一个tag为“MainCamera”的相机,并且返回。这个寻找是遍历所有的tag,并且不会被缓存的。

该类的方法中存在ComputeBuffer.GetData的调用

这个是从GPU的buffer中读取对应的计算结果并输入到相应的数据中,由于整个过程是一个同步的操作,所以调用的时候会堵塞调用线程,直到GPU返回数据为止。

该类中存在对纹理SetPixels的调用

SetPixels用于对纹理特定的mipmap层的像素进行修改,它会将一组数据的像素值赋值到贴图的指定mipmap层,调用Apply()后将像素传到显卡。

该类的方法中存在GameObject.SendMessage调用

这个方法会遍历Gameobject上所有的组件,以及组件中的所有函数,会导致很高的cpu消耗。

该类的方法中存在GetComponentsInChindren调用/GetCompoentsInParent调用

这两个的使用都会涉及到较大范围内的搜索遍历。Unity在GameObject的GetComponentsInChildren的方法是支持传入一个List的,这个是可以减少堆内存的分配。

调用了FindObjectsOfType调用

这个会对于场景中的GameObject和Component进行遍历,并将与目标type类型相同的组件以数组的方式返回。这个方法还会产生对堆内存的分配。

存在Reflection相关函数的调用

在调用反射相关的方法时,需要获取类型与函数等信息,并且进行参数校验,错误处理,安全性检查等。还会造成堆内存的分配。

存在对Renderer进行Material/Materials的获取

如果对renderer类型调用.material和.materials,那么unity就会生成新的材质球实例。所以我们可以使用MaterialPropertyBlock来替代Material属性的操作。使用相同的Shader但是Material实例不同的GameObject是无法进行合批操作,所以dc增加了,也就增加了cpu的消耗。
每次调用.material都会生成一个新的Material实例,并且GameObject销毁后,Material的实例是没有办法进行自动销毁的。所以需要手动调用“UnloadUnusedAssets"来进行卸载,就会添加性能开销;如果管理不好的话,内存也会很高。

Ugui中UpdateBatches占用耗时较高

在ugui的UI元素刷新是基于canvas下的,所以当我们ui上的数据做SetActive的时候是会把Canvas下的所有元素都触发了,可以看到Rendering.UpdateBatches的调用会比较多

Ugui中的Rebatch优化

Rebatch发生在c++层面,指的是Canvas分析UI节点生成最优批次的过程,节点数过多会导致算法(贪心)的耗时比较长。对应SetVerticesDirty,当一个canvas中包含的mesh发生改变就会触发(setactivity,transform,颜色,文本内容等),canvas独立处理,互相不影响的,消耗在meshes按照深度和重叠情况排序,共享材质的检测。
优化点:

  1. Canvas动静分离,合理规划,不能太多(会提高DC的消耗)
  2. 减少节点层次和数量,合批计算量小,速度快。
  3. 使用相同材质贴图的ui尽量保持深度相同。

Ugui中的Rebuild

rebuild发生在c#层面,是ugui库中的layout组件调整RectTransform尺寸,Graphic组件更新material,以及mask执行Cull的过程,耗时和发生变化的节点数量基本呈现线性相关。

只有LayoutGroup的直接子节点,并且是 Graphic类型的(比如 Image 和 Text)会触发SetLayoutDirty。

Graphic改变的原因包括,基本的大小、旋转以及文字的变化、图片的修改等等,对应SetMaterialDirty。
优化点:

  1. 少用layout

堆内存优化

  • 无法及时的释放内存。
  • 过多的分配次数会到时内存中的碎片过多,从而无法开辟所需要的连续内存。
  • 内存的GC会照成卡顿

存在.tag的调用

获取tag的时候实际上是调用了get_tag()函数,从native层返回一个字符串,字符串的返回会造成堆内存的分配,unity也没有通过缓存的方法来做优化。

该类中存在对纹理的GetPixels()/GetPixels32调用

一般是为了获取指定mipmap层的全部像素信息,而图片上的像素往往是很庞大的。
会在堆内存上分配内存,用来存储纹理数据的像素信息,而且引擎不会对其进行缓存。

使用了Linq相关的函数调用

linq在执行过程中会产生一些临时变量,而且会用到委托。如果使用委托作为条件的判断方式,时间开销就会很高,并且会造成一定的堆内存分配。

存在对Renderer进行sharedMaterials的获取

对.sharedMaterials的调用,依旧会分配堆内存,每次调用都会分配一个用来存放Material的索引的数组。

存在Input.touches调用

.touches的实现是每次都会new一个数组touches,从而造成一定的堆内存分配。

存在对TextAsset调用

在获取bytes属性的时候,Unity会从Native层获取字节数组(byte[]),从而分配一定的堆内存

C#和lua的穿插引用

c#层会维护一个cache来引用那些被lua访问过的C#层对象,这是为了防止当lua中再次访问该c#对象时,这个对象已经被c#的gc给回收掉了。但是如果lua始终保持着对某个c#层对象的引用,那讲会导致无法被释放。造成堆内存泄漏。