1、前言
最近在Unity中做一个数据库的连接及数据的统计功能,统计的数据用ScrollView 来滚动查看,发现数据量少,滚动没有卡顿,数据量在1000条左右往上,滚动视图时,就会发现整个列表出现了明显的卡顿。通过Profiler分析,滚动列表时造成了大量的计算、渲染以及垃圾回收,从而造成了卡顿。
我最开始的数据库统计功能思路是,打开视图,先查询并获取数据库中的所有数据,根据数据量,来初始化列表预制件,并将数据对应的数据结构赋值给每个列表预制件,并将每个预制件设置为激活状态。实测发现,数据量大的时候,打开视图包括滚动都存在严重的卡顿。
2、优化方法及代码
查阅了一下资料,大多数优化方法是实时更新ScrollView中可见的列表项,减少计算的次数,尽可能节约资源,将需要显示的进行激活及显示,其余列表项不进行显示和激活,下面分享一下我优化的过程和遇到的问题。
专门设置了一个单例类来管理列表项,包括实例化列表,每帧更新可见项等,初始化传入一个content节点来完成。这个管理类的代码如下:
c#
using System;
using System.Collections.Generic;
using Libs.Base;
using UnityEngine;
/// <summary>
/// 数据项管理
/// </summary>
public class DataItemManager : Singleton<DetectionDataItemManager>
{
/// <summary>
/// 默认初始化数量
/// </summary>
private const int kDefaultNum = 200;
/// <summary>
/// 可见列表数量
/// </summary>
private const int kVisibleItemsAmount = 15;
/// <summary>
/// 所有实例化的数据项集合
/// </summary>
private List<Transform> m_DataGameObjects;
/// <summary>
/// 当前赋值的数据项集合
/// </summary>
private List<Transform> m_CurGameObjects;
/// <summary>
/// 可见的数据项集合
/// </summary>
private List<Transform> m_VisibleItemsList;
/// <summary>
/// 列表数据项预制件
/// </summary>
private Transform m_DataPrefab;
/// <summary>
/// 父节点
/// </summary>
private Transform m_ParenTransform;
/// <summary>
/// 记录上一次content的坐标y值
/// </summary>
private float m_LastY;
/// <summary>
/// 每个列表预制件的高度
/// </summary>
private float m_ItemHeight;
/// <summary>
/// 初始化
/// </summary>
/// <param name="parentTransform">The parent transform.</param>
public void Init(Transform parentTransform)
{
m_ParenTransform = parentTransform;
m_LastY = -10;//这里是为了每次查询后确定每帧更新,数据进行刷新
m_DataGameObjects = new List<Transform>();
m_CurGameObjects = new List<Transform>();
m_VisibleItemsList = new List<Transform>();
m_DataPrefab = parentTransform.Find("DetectionItem").transform;
m_ItemHeight = m_DetectionPrefab.GetComponent<RectTransform>().rect.height;
m_ParenTransform.GetComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.Unconstrained;
InitNum(kDefaultNum);
}
/// <summary>
/// 每帧更新(在主视图中调用这个方法来每帧更新)
/// </summary>
public void Update()
{
UpdateVisibleItems();
}
/// <summary>
/// 获取可见的数据项集合
/// </summary>
/// <param name="num">The num.</param>
/// <returns>激活的检测数据项集合.</returns>
public List<Transform> GetDataItems(int num)
{
HideAllObjects();
m_CurGameObjects.Clear();
m_VisibleItemsList.Clear();
while (m_DataGameObjects.Count < num)
{
InitNum(kDefaultNum);
}
for (int i = 0; i < num; i++)
{
m_CurGameObjects.Add(this.m_DataGameObjects[i]);
}
m_LastY = -10;
//根据数据量来设置content的高度
m_ParenTransform.GetComponent<RectTransform>().sizeDelta =
new Vector2(0, DetectionDataItemManager.instance.GetItemHeight() * curDtaList.Count);
return m_CurGameObjects;
}
/// <summary>
/// 隐藏所有数据项集合
/// </summary>
public void HideAllObjects()
{
var activeObjects = m_DataGameObjects.FindAll(go => go.gameObject.activeSelf);
foreach (var dataGameObject in activeObjects)
{
dataGameObject.gameObject.SetActive(false);
}
}
/// <summary>
/// 获取列表项的高度
/// </summary>
/// <returns>The <see cref="float"/>.</returns>
public float GetItemHeight()
{
return m_ItemHeight;
}
/// <summary>
/// 初始化数据项集合
/// </summary>
/// <param name="num">The num.</param>
private void InitNum(int num)
{
var location = m_DataPrefab.localPosition;
var count = m_DataGameObjects.Count;
var y = count > 1 ? m_DataGameObjects[count - 1].localPosition.y - m_ItemHeight : -m_ItemHeight / 2;
for (int i = 0; i < num; i++)
{
var instanceObj = UnityEngine.Object.Instantiate(this.m_DataPrefab);
instanceObj.transform.SetParent(this.m_ParenTransform);
instanceObj.transform.localPosition = new Vector3(location.x, y, location.z);
y -= m_ItemHeight;
instanceObj.gameObject.SetActive(false);
instanceObj.transform.localScale = Vector3.one;
m_DataGameObjects.Add(instanceObj);
}
}
/// <summary>
/// 每帧更新可见项
/// </summary>
private void UpdateVisibleItems()
{
if (Math.Abs(m_LastY - m_ParenTransform.localPosition.y) < 1e-3)
{
//未滚动,不更新
return;
}
var firstVisibleIndex = Mathf.FloorToInt(m_ParenTransform.localPosition.y / m_ItemHeight);
m_LastY = m_ParenTransform.localPosition.y;
for (int i = 0; i < m_CurGameObjects.Count; i++)
{
if (i >= firstVisibleIndex && i < firstVisibleIndex + kVisibleItemsAmount)
{
if (!m_VisibleItemsList.Contains(m_CurGameObjects[i]))
{
m_VisibleItemsList.Add(m_CurGameObjects[i]);
m_CurGameObjects[i].gameObject.SetActive(true);
}
}
else
{
if (m_VisibleItemsList.Contains(m_CurGameObjects[i]))
{
m_VisibleItemsList.Remove(m_CurGameObjects[i]);
m_CurGameObjects[i].gameObject.SetActive(false);
}
}
}
}
}
3、过程中需要注意的问题
实例化每个预制件时,没有指定预制件的位置Localposition,通过在content上挂载一个Vertical Layout Group组件来进行垂直布局的管理,使每个列表项在垂直方向整齐均匀排列。后面发现列表起始位置显示没有问题,但是滑动之后跟着不见了。在Hierarchy视图中查看,发现滚动视图滑动之后整个content是往上在移动的,列表项始终从content的左上角开始排列,所以滚动后列表项没有像预想中正确显示在ScrollView中。
这里的问题在于如果挂载了VerticalLayoutGroup组件,它只会对已经激活的gameObject进行排序,所以我们想要显示的可见项列表会始终从规定的左上角开始排列,一旦滚动视图,列表超出了ScrollView范围,就不会可见或部分可见。
解决办法是去掉VerticalLayoutGroup组件,增加ContentSizeFitter组件,并将垂直适应属性修改为Unconstrained。
c#
//初始化时添加ContentSizeFitter组件,并将verticalFit设置为Unconstrained
m_ParenTransform.GetComponent<ContentSizeFitter>().verticalFit =
ContentSizeFitter.FitMode.Unconstrained;
这种情况下我们需要确定content的RectTransform的高度,来容纳实例化的预制件,高度由每次需要的列表数及每个列表的高度确定。
c#
//根据数据量来设置content的高度
m_ParenTransform.GetComponent<RectTransform>().sizeDelta =
new Vector2(0, DetectionDataItemManager.instance.GetItemHeight() * curDtaList.Count);
另外没有了VerticalLayoutGrou,实例化的列表预制件就堆叠在了一起,我们需要在实例化预制件时就指定每个预制件的Localposition。
c#
/// <summary>
/// 初始化数据项集合
/// </summary>
/// <param name="num">The num.</param>
private void InitNum(int num)
{
var location = m_DataPrefab.localPosition;
var count = m_DataGameObjects.Count;
//起始位置的y值确定
var y = count > 1 ? m_DataGameObjects[count - 1].localPosition.y - m_ItemHeight : -m_ItemHeight / 2;
for (int i = 0; i < num; i++)
{
var instanceObj = UnityEngine.Object.Instantiate(this.m_DataPrefab);
instanceObj.transform.SetParent(this.m_ParenTransform);
instanceObj.transform.localPosition = new Vector3(location.x, y, location.z);
y -= m_ItemHeight;
instanceObj.gameObject.SetActive(false);
instanceObj.transform.localScale = Vector3.one;
m_DataGameObjects.Add(instanceObj);
}
}
这样一来,我们实例化的列表项均有自己确定的位置,不需要布局组件来排列,激活时就按位置显示出来。
最后一步就是每帧更新的逻辑,我们需要确定一个显示的列表数量,例如ScrollView高度300,7列表项高度30,这样这个ScrollView中一次可以显示10个列表,那么我们将可显示的列表项最少为10。每帧更新时获取content的y值坐标(垂直滚动),将y值除以列表的高度来获取当前显示的列表项的起始索引,加上显示列表项数量,去集合中查找对应索引的gameObject,加入可见列表项集合并激活,索引外的就移出可见列表项集合,并设置为未激活。
c#
/// <summary>
/// 每帧更新可见项
/// </summary>
private void UpdateVisibleItems()
{
if (Math.Abs(m_LastY - m_ParenTransform.localPosition.y) < 1e-3)
{
//未滚动,不更新
return;
}
//计算起始索引
var firstVisibleIndex = Mathf.FloorToInt(m_ParenTransform.localPosition.y / m_ItemHeight);
//这里记录一下上次的y值,判断是否发生了滚动
m_LastY = m_ParenTransform.localPosition.y;
for (int i = 0; i < m_CurGameObjects.Count; i++)
{
if (i >= firstVisibleIndex && i < firstVisibleIndex + kVisibleItemsAmount)
{
//索引在可见区间
if (!m_VisibleItemsList.Contains(m_CurGameObjects[i]))
{
m_VisibleItemsList.Add(m_CurGameObjects[i]);
m_CurGameObjects[i].gameObject.SetActive(true);
}
}
else
{
//索引不在可见区间
if (m_VisibleItemsList.Contains(m_CurGameObjects[i]))
{
m_VisibleItemsList.Remove(m_CurGameObjects[i]);
m_CurGameObjects[i].gameObject.SetActive(false);
}
}
}
}
4、结语
上述只是优化的其中一种方法,可根据自己的应用场景使用延迟加载、缓存、协程等进行优化。