UGUI ScrollRect 性能优化
测试环境
操作系统:Windows8.1
开发工具:Unity5.5.2
1、问题描述,在实际开发过程中经常会使用ScrollRect实现滚动列表,当初次加载数据比较多的情形时,Unity3D会出现比较严重的卡顿,降低帧率,其原因主要为 a、集中式的申请ItemRenderer对象大量堆内存,b、帧Draw Call增加。
2、解决方案,主要逻辑根据Viewport即Mask Visual区域计算当前上下文显示ItemRenderer个数,同时滚动的时候会动态计算scrollLineIndex行数,来重新计算每一个ItemRenderer渲染的位置,从而复用ItemRenderer。
如图所示:当前数据已经有31个,但是ItemRenderer的实例只有21个,即当前满屏情况下最大的显示个数。
3、完成代码
UIWrapItem 用来作为数据、ItemRenderer prefab的 具体关联类。
using UnityEngine; /// <summary> /// Wrapper Item for user data index. /// </summary> public class UIWrapItem : MonoBehaviour { /// <summary> /// User data index /// </summary> private int dataIndex = 0; /// <summary> /// Item container /// </summary> private UIWrapContainer container = null; private void OnDestroy() { container = null; } /// <summary> /// Container setter /// </summary> public UIWrapContainer Container { set { container = value; } } /// <summary> /// DataIndex getter & setter /// </summary> public int DataIndex { set { dataIndex = value; gameObject.name = dataIndex.ToString(); if (dataIndex >= 0) { transform.localPosition = container.GetLocalPositionByDataIndex(dataIndex); if (container.onInitializeItem != null) { container.onInitializeItem(gameObject, dataIndex); } } } get { return dataIndex; } } }
UIWrapContainer作用为UIWrapItem的容器,提供基本的数据判定逻辑。
using UnityEngine; using UnityEngine.UI; using System.Collections.Generic; /// <summary> /// This script makes it possible for a scroll view to wrap its item, creating scroll views. /// Usage: simply attach this script underneath your scroll view where you would normally place a container: /// /// + Scroll View /// |- UIWrapContainer /// |-- Item 1 /// |-- Item 2 /// |-- Item 3 /// </summary> [DisallowMultipleComponent] public class UIWrapContainer : MonoBehaviour { public delegate void OnInitializeItem(GameObject go, int dataIndex); public OnInitializeItem onInitializeItem = null; public enum Arraygement { Horizontal, Vertical } #region public variables /// <summary> /// Type of arragement /// </summary> public Arraygement arrangement = Arraygement.Horizontal; /// <summary> /// Maximum item per line. /// If the arrangement is horizontal, this denotes the number of columns. /// If the arrangement is vertical, this stands for the number of rows. /// </summary> public int maxPerLine = 1; /// <summary> /// The width of each of the items. /// </summary> public float itemWidth = 100f; /// <summary> /// The height of each of the items. /// </summary> public float itemHeight = 100f; /// <summary> /// The Horizontal space of each of the items. /// </summary> public float itemHorizontalSpace = 0f; /// <summary> /// The vertical space of each of the items. /// </summary> public float itemVerticalSpace = 0f; public ScrollRect scrollRect = null; public RectTransform viewport = null; public RectTransform container = null; public GameObject itemPrefab = null; #endregion #region private variables private int dataCount = 0; private int maximumVisualVerticalItemCount = 0; private int maximumVisualHorizontalItemCount = 0; private int currentScrollLineIndex = -1; private IList<UIWrapItem> activeItems = null; private Queue<UIWrapItem> deactiveItems = null; #endregion void Awake() { activeItems = new List<UIWrapItem>(); deactiveItems = new Queue<UIWrapItem>(); maximumVisualVerticalItemCount = Mathf.CeilToInt(viewport.rect.height / (itemHeight + itemVerticalSpace)); maximumVisualHorizontalItemCount = Mathf.CeilToInt(viewport.rect.width / (itemWidth + itemHorizontalSpace)); } void Start() { } public void Initialize(int dataCount) { if (scrollRect == null || container == null || itemPrefab == null) { Debug.LogError("Not attach scrollRect or container or itemPrefab instance here, please check."); return; } if (dataCount <= 0) { return; } setDataCount(dataCount); scrollRect.onValueChanged.RemoveAllListeners(); scrollRect.onValueChanged.AddListener(OnValueChanged); deactiveItems.Clear(); activeItems.Clear(); UpdateItems(0); } public void RemoveItem(int dataIndex) { if (dataIndex < 0 || dataIndex >= dataCount) { return; } bool isDestroy = activeItems.Count >= dataCount; setDataCount(dataCount - 1); for (int index = activeItems.Count - 1; index >= 0; index--) { UIWrapItem item = activeItems[index]; int itemDataIndex = item.DataIndex; if (itemDataIndex == dataIndex) { activeItems.Remove(item); if (isDestroy) { GameObject.Destroy(item.gameObject); } else { item.DataIndex = -1; item.gameObject.SetActive(false); deactiveItems.Enqueue(item); } } if (itemDataIndex > dataIndex) { item.DataIndex = itemDataIndex - 1; } } UpdateItems(GetCurrentScrollLineIndex()); } public void AddItem(int dataIndex) { if (dataIndex < 0 || dataIndex > dataCount) { return; } bool isNew = false; for (int index = activeItems.Count - 1; index >= 0; index--) { UIWrapItem item = activeItems[index]; if (item.DataIndex >= (dataCount - 1)) { isNew = true; break; } } setDataCount(dataCount + 1); if (isNew) { for (int index = 0, length = activeItems.Count; index < length; index++) { UIWrapItem item = activeItems[index]; int itemDataIndex = item.DataIndex; if (itemDataIndex >= dataIndex) { item.DataIndex = itemDataIndex + 1; } } UpdateItems(GetCurrentScrollLineIndex()); } else { for (int index = 0, length = activeItems.Count; index < length; index++) { UIWrapItem item = activeItems[index]; int itemDataIndex = item.DataIndex; if (itemDataIndex >= dataIndex) { item.DataIndex = itemDataIndex; } } } } public Vector3 GetLocalPositionByDataIndex(int dataIndex) { float x = 0f; float y = 0f; float z = 0f; switch (arrangement) { case Arraygement.Horizontal: { x = (dataIndex / maxPerLine) * (itemWidth + itemHorizontalSpace); y = -(dataIndex % maxPerLine) * (itemHeight + itemVerticalSpace); break; } case Arraygement.Vertical: { x = (dataIndex % maxPerLine) * (itemWidth + itemHorizontalSpace); y = -(dataIndex / maxPerLine) * (itemHeight + itemVerticalSpace); break; } } return new Vector3(x, y, z); } private void setDataCount(int count) { if (count != dataCount) { dataCount = count; UpdateContainerSize(); } } private void OnValueChanged(Vector2 value) { switch (arrangement) { case Arraygement.Vertical: { float y = value.y; if (y >= 1.0f || y <= 0.0f) { return; } break; } case Arraygement.Horizontal: { float x = value.x; if (x <= 0.0f || x >= 1.0f) { return; } break; } } int scrollPerLineIndex = GetCurrentScrollLineIndex(); if (scrollPerLineIndex == currentScrollLineIndex) { return; } UpdateItems(scrollPerLineIndex); } private void UpdateItems(int scrollLineIndex) { if (scrollLineIndex < 0) { return; } currentScrollLineIndex = scrollLineIndex; int startDataIndex = currentScrollLineIndex * maxPerLine; int endDataIndex = (currentScrollLineIndex + (arrangement == Arraygement.Vertical ? maximumVisualVerticalItemCount : maximumVisualHorizontalItemCount)) * maxPerLine; for (int index = activeItems.Count - 1; index >= 0; index--) { UIWrapItem item = activeItems[index]; int itemDataIndex = item.DataIndex; if (itemDataIndex < startDataIndex || itemDataIndex >= endDataIndex) { item.DataIndex = -1; activeItems.Remove(item); item.gameObject.SetActive(false); deactiveItems.Enqueue(item); } } for (int dataIndex = startDataIndex; dataIndex < endDataIndex; dataIndex++) { if (dataIndex >= dataCount) { continue; } if (Exists(dataIndex)) { continue; } CreateItem(dataIndex); } } private void CreateItem(int dataIndex) { UIWrapItem item = null; if (deactiveItems.Count > 0) { item = deactiveItems.Dequeue(); item.gameObject.SetActive(true); } else { item = AddChild(itemPrefab, container).AddComponent<UIWrapItem>(); } item.Container = this; item.DataIndex = dataIndex; activeItems.Add(item); } private bool Exists(int dataIndex) { if (activeItems == null || activeItems.Count <= 0) { return false; } for (int index = 0, length = activeItems.Count; index < length; index++) { if (activeItems[index].DataIndex == dataIndex) { return true; } } return false; } private GameObject AddChild(GameObject prefab, Transform parent) { if (prefab == null || parent == null) { Debug.LogError("Could not add child with null of prefab or parent."); return null; } GameObject go = GameObject.Instantiate(prefab) as GameObject; go.layer = parent.gameObject.layer; go.transform.SetParent(parent, false); return go; } private void OnDestroy() { scrollRect = null; container = null; itemPrefab = null; onInitializeItem = null; activeItems.Clear(); deactiveItems.Clear(); activeItems = null; deactiveItems = null; } }
4、未完待续
实际在开中主要使用数组集合作为参数及动态的Add or Rmove操作,所以接下来将会迭代掉采用数组下标的方式提供数据的初始化,增加删除等操作。
最后实现仓促难免存在bug,请与指正。