概述
当前项目是一个类似scratch那种积木式的编程工具,编辑器用Unity的uGUI实现,但是对于大型的的工程(600多个block,每个block大概有5、6个GameObject),原有的实现在加载工程时很慢,profiler中查看,发现有很大一部分时间花在了RectTransform.SetParent
调用上,这几天花了点时间,优化了这部分调用。
原因
RectTransform.SetParent
调用,会触发许多其他的调动,比如Graphic.OnBeforeTranformParentChange
,由于工程比较大,场景中有大量的GameObjects,许多SetParent
调用导致了加载性能的降低。
原有的工程加载步骤为
- 实例化所有GameObjects
- Load保存的数据
- 恢复GameObjects之间的引用,此时会调用
SetParent
重新恢复父子关系
由于最初序列化数据结构设计的问题,无法在实例化时知道parent是谁,所以导致Instantiate
调用时不能传入parent
参数来减少不必要的开销。
解决方法
想到的一个方法是不依赖于Transform
的父子关系,而是手动计算所有的坐标,这样我们在实例化后就不需要调用SetParent
,就不会有UI的开销了。
为了保持当前代码基本结构不变,即使用本地坐标的地方可以依然指定本地坐标,引入了一个逻辑坐标系的概念(LogicTransform
)。逻辑坐标系的层级结构同原来使用unity的Transform
建立的层级结构,主要的区别在与我们自己计算整个层级中的坐标,而不是unity代劳。加上我们的ui不需要旋转,只涉及缩放和平移,实际计算代码很简单。下面是一个GameObjects的层级,其中黑色箭头组成的逻辑层级,红色箭头组成的unity的Transform
的层级。
每个LogicTransform
有一个对应的Transform
,两者之间满足如下关系
LogicTransform.localScale == Transform.localScale
Transform
在逻辑层级中本地坐标(非localPositon
),等于LogicTransform.worldPosition
采用这种实现后,工程加载的时间从最初的30s降低到了15s(依然很慢)。
优化ReapplyDrivenProperties
Profiler中还发现,RectTransform.reapplyDrivenProperties也很费时,查看了uGUI的源码发现,这个部分开销来源于LayoutRebuilder
在该事件处理中,调用了MarkForLayoutRebuild
所致。由于我们的block布局完全是手动的,所以这部分开销也可以省掉。但是LayoutRebuilder
的事件处理是私有的,所幸可以通过反射将事件处理函数替换掉。替换后的逻辑如下
static void ReapplyDrivenProperties(RectTransform driven)
{
if (driven != null && ((1 << driven.gameObject.layer) & layersToIgnore != 0))
{
return;
}
else
{
// call the original event handler in LayoutRebuilder
originalHandler(driven)
}
}
这里将不需要自动layout的game objects分配到特殊的layer上避免开销。这步优化带来的提升不是很明显,大概节省了1.5s左右的时间。
优化SetActive调用
有些元素的显示和隐藏通过SetActive来完成。由于每次调用会触发OnEnable/OnDisable
,在game objects相对较多的情况下,开销也不小,所以后来使用嵌套Canvas的方法来解决。改动后,加载速度有所提升。
结论
- 大量调用
RectTransform.SetParent
对性能有负面影响 - 避免不必要的
SetActive
,使用嵌套Canvas也可以提升部分性能 - 设计良好的Save数据结构应该可以解决重新
SetParent
带来的开销