Unity中ScrollView滚动列表卡顿优化

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、结语

上述只是优化的其中一种方法,可根据自己的应用场景使用延迟加载、缓存、协程等进行优化。

相关推荐
r i c k18 分钟前
数据库系统学习笔记
数据库·笔记·学习
野犬寒鸦32 分钟前
从零起步学习JVM || 第一章:类加载器与双亲委派机制模型详解
java·jvm·数据库·后端·学习
IvorySQL1 小时前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
·云扬·1 小时前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
IT邦德2 小时前
Oracle 26ai DataGuard 搭建(RAC到单机)
数据库·oracle
惊讶的猫2 小时前
redis分片集群
数据库·redis·缓存·分片集群·海量数据存储·高并发写
不爱缺氧i2 小时前
完全卸载MariaDB
数据库·mariadb
纤纡.2 小时前
Linux中SQL 从基础到进阶:五大分类详解与表结构操作(ALTER/DROP)全攻略
linux·数据库·sql
jiunian_cn2 小时前
【Redis】渐进式遍历
数据库·redis·缓存
橙露3 小时前
Spring Boot 核心原理:自动配置机制与自定义 Starter 开发
java·数据库·spring boot