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

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

相关推荐
日里安40 分钟前
8. 基于 Redis 实现限流
数据库·redis·缓存
EasyCVR1 小时前
ISUP协议视频平台EasyCVR视频设备轨迹回放平台智慧农业视频远程监控管理方案
服务器·网络·数据库·音视频
Elastic 中国社区官方博客1 小时前
使用真实 Elasticsearch 进行更快的集成测试
大数据·运维·服务器·数据库·elasticsearch·搜索引擎·集成测试
明月与玄武2 小时前
关于性能测试:数据库的 SQL 性能优化实战
数据库·sql·性能优化
PGCCC3 小时前
【PGCCC】Postgresql 存储设计
数据库·postgresql
PcVue China5 小时前
PcVue + SQL Grid : 释放数据的无限潜力
大数据·服务器·数据库·sql·科技·安全·oracle
魔道不误砍柴功7 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
锐策7 小时前
〔 MySQL 〕数据库基础
数据库·mysql
远歌已逝8 小时前
管理Oracle实例(二)
数据库·oracle
日月星宿~8 小时前
【MySQL】summary
数据库·mysql