问题背景:
在工作中时常会遇到 Layout Group 下需要动态生成若干对象的情况,在此时如果对象生成数量过多的话,会导致性能下降严重(内存占用过高、运行卡顿、刷新过慢等)。
技术思想:
此时一般会采用对象池技术来重复利用已经生成的对象。对于超出视觉范围的无需刷新的对象进行回池处理,当需要增加新对象时,优先从对象池中取出生成过的对象使用,只有当对象池为空时才会主动申请新内存生成新对象。
ItemPool
使用到的Unity中的工具:
组件 Horizontal Layout Group 或者 Vertical Layout Group,
组件 Content Size Fitter,
组件 ScrollRect
以及滑动滚动时触发的监听 ScrollRect.onValueChanged
(对于 LayoutGroup 组件和 ContentSizeFitter 的详细介绍可参考这篇文章)Unity LayoutGroup和ContentSizeFitter的效果及简单应用案例_control child size,use child scale ,child force ex-CSDN博客
实现细节:
问题的核心在于判断什么时候需要生成新的对象or到对象池中取对象出来,以及对象什么时候回池(也就是何时离开视觉范围)
0.首先我们需要实现一个判断当前物体是否在视觉范围的方法 CheckRectIsOverRect;
1.我们初始化LayoutGroup,在其中使用数个占位空物体 ItemParent 来标记每个物体的位置;
2.滑动时每帧对每个 ItemParent 进行位置判定,进入范围时加载一个对象 Item 作为其子物体,离开范围时将自己的子物体 Item 返回至Item对象池,对于没有发生进入或者离开范围的 ItemParent 不做处理。同时在 Item 发生进入范围的时刻对 Item 进行刷新;
3.如果需要动态改变当前Item的总数量,则需要对ItemParent也进行回池处理。
示例代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class TestItemPool : MonoBehaviour
{
private class Item
{
public ItemParent parent; // 父物体引用
public GameObject gameObject;
public RectTransform rectTransform;
public Text txt;
}
private class ItemParent
{
public Item item; // 子物体引用
public GameObject gameObject;
public RectTransform rectTransform;
public int index = -1;
public bool isShowed = false; // 是否显示过
public bool isInRect = false; //
}
public ScrollRect scrollRect;
public RectTransform contentRectTrans;
public GameObject itemParentPrefab;
public GameObject itemPrefab;
public GameObject nodePool;
private List<Item> items = new List<Item>(); // 存放在激活状态中的Item
private List<Item> itemPool = new List<Item>(); // 存放回收Item的对象池
private List<ItemParent> itemParents = new List<ItemParent>(); // 存放在激活状态中的ItemParent
private List<ItemParent> itemParentPool = new List<ItemParent>(); // 存放回收ItemParent的对象池
// Start is called before the first frame update
void Start()
{
this.CreateDynamicList();
this.UpdateListByData(10); // 创造10个占位
}
// 从物体对象池中取出Item, 并将item设置为itemParent的子物体
Item GetItemFromPool(ItemParent itemParent)
{
Item item;
if (this.itemPool.Count ==0)
{
item = new Item();
item.gameObject = GameObject.Instantiate(this.itemPrefab, itemParent.rectTransform);
item.rectTransform = item.gameObject.GetComponent<RectTransform>();
}
else
{
item = this.itemPool[0];
this.itemPool.RemoveAt(0);
item.rectTransform.SetParent(itemParent.rectTransform);
}
itemParent.item = item;
item.parent = itemParent;
item.rectTransform.localPosition = Vector3.zero;
item.txt = item.gameObject.GetComponentInChildren<Text>();
return item;
}
// 将Item返回至对象池;
void PushItemToPool(Item item)
{
if (item == null)
{
return;
}
this.items.Remove(item);
item.rectTransform.SetParent(this.nodePool.transform);
item.parent.item = null; // 移除父子关系引用
item.parent = null; // 移除父子关系引用
this.itemPool.Add(item);
}
// 从父物体对象池中取出ItemParent
ItemParent GetItemParentFromPool()
{
ItemParent curItemParent;
if (this.itemParentPool.Count == 0) // 如果没有,则生成一个新的返回
{
curItemParent = new ItemParent();
curItemParent.gameObject = GameObject.Instantiate(this.itemParentPrefab, this.contentRectTrans);
curItemParent.rectTransform = curItemParent.gameObject.GetComponent<RectTransform>();
} else
{
curItemParent = this.itemParentPool[0];
this.itemParentPool.RemoveAt(0);
curItemParent.rectTransform.SetParent(this.contentRectTrans);
}
return curItemParent;
}
// 将父物体对象回池
void PushItemParentToPool(ItemParent itemParent)
{
this.PushItemToPool(itemParent.item); // 将父物体的子物体进行回池
itemParent.index = -1;
itemParent.isInRect = false;
itemParent.isShowed = false;
this.itemParentPool.Add(itemParent) ;
}
// 根据数据生成数量上限的Node_ItemParent, 用来构造布局
void UpdateListByData(int len)
{
// 如果不足,补充显示
for (int i = this.itemParents.Count; i<= len-1; i++)
{
this.itemParents.Add(this.GetItemParentFromPool());
this.itemParents[i].index = i;
this.itemParents[i].rectTransform.name = ""+i;
}
// 多余的回池
for (int i = this.itemParents.Count-1; i>= len; i--)
{
this.PushItemParentToPool(this.itemParents[i]);
this.itemParents.RemoveAt(i);
}
UnityEngine.UI.LayoutRebuilder.ForceRebuildLayoutImmediate(this.contentRectTrans);
this.UpdateScrolling();
}
// 注册滚动事件
void CreateDynamicList()
{
this.contentRectTrans = this.scrollRect.content.gameObject.GetComponent<RectTransform>();
this.scrollRect.onValueChanged.AddListener(value=> { this.UpdateScrolling(); });
}
// 滚动时触发的函数刷新
void UpdateScrolling()
{
// 检查所有在框范围内的激活状态的ItemParent
for (int i = 0; i< this.itemParents.Count; i++)
{
bool isInRect = CheckRectIsOverRect(this.itemParents[i].rectTransform, this.scrollRect.gameObject.GetComponent<RectTransform>());
if (this.itemParents[i].isShowed == false) // 如果是第一次进入
{
this.itemParents[i].isInRect = isInRect;
this.itemParents[i].isShowed = true;
if (isInRect)
{
this.GetItemFromPool(this.itemParents[i]);
this.UpdateItem(this.itemParents[i].item, i);// 刷新item
}
} else // 如果不是第一次进入
{
if ((isInRect == false) && this.itemParents[i].isInRect) // 之前在范围内,现在离开
{
this.itemParents[i].isInRect = isInRect;
this.PushItemToPool(this.itemParents[i].item);
}
else if (isInRect && (this.itemParents[i].isInRect == false)) // 之前不在范围内,现在进入
{
this.itemParents[i].isInRect = isInRect;
this.GetItemFromPool(this.itemParents[i]);
this.UpdateItem(this.itemParents[i].item, i);// 刷新item
}
}
}
}
// 检查aRectTransform 是否和 bRectTransform 相交,或在其之内
bool CheckRectIsOverRect(RectTransform aRectTransform, RectTransform bRectTransform)
{
float delta_x = Mathf.Abs(aRectTransform.position.x - bRectTransform.position.x);
float delta_y = Mathf.Abs(aRectTransform.position.y - bRectTransform.position.y);
if (delta_x <= aRectTransform.sizeDelta.x / 2 + bRectTransform.sizeDelta.x / 2 &&
delta_y <= aRectTransform.sizeDelta.y / 2 + bRectTransform.sizeDelta.y / 2)
{
return true;
}
return false;
}
void UpdateItem(Item item, int i)
{
item.txt.text = ""+i;
}
}
记录一个现象:
在滑动速度较快时,可能同时会有显示中的对象数量离开范围回池,同时有下一帧会显示出来的对象数量被刷新出来,这意味着此时内存中所有的对象个数最多可能会达到两倍于同时显示的对象数量。目前暂未处理该现象。