问题描述:
原先商城的物品栏中的item 是load在一个scrollView 下,用于滑动查看。仅仅在父级panel下是使用了NGUI原生的scrollview 组件,随着商场物品列表中新物品的增多。panel下加载的实例也非常庞大。而大部分的实例用户也无法看到(scrollView工作原理是只渲染了content区域内的实例),可以看到item.get方法每次也需跑满循环,一帧的情况下调用多达数万次。
解决思路:
于是,问题的根源来自于我们创建了太多实例。
那么实际上需不要要创建那么多呢,答案是否定的。对于每个item物品信息,实例本身是可以复用的,不同点在于它初始化的数据不一样,以及详情点击、以及右侧展示面板的信息不一样
那么设想,我们是否可以在滑动到指定位置时,重新初始化实例的信息,达到类似 ”滚动视图“的障眼法呢。
RecyclableScrollView自定义组件实现
原理以及主要组成部分
循环列表组件基于NGUI框架开发,可支持多行或多列的循环列表。仅创建少数Item,通过变更位置的方式实现的Item复用,以优化性能损耗。
原理如下(双端队列)
上滑时判断【队头元素是否需要移动到队尾】
第一屏实际只加载了k(图中k = 9)个item,在向上滚动时,当第【1】个item出了显示的范围,第【1】个transform.position += k * item.height,就变即将要显示的第【10】个了。
继续上滑,下面依次判断第【2】【3】…是否需要下移至末尾
下滑时同理,判断【队尾元素是否需要移动到对头】
主要的核心组件为RecyclableScrollView和RecyclableGridControl:
使用方法
- 面板中依次创建Panel->Grid结点,并在添加RecyclableScroll View.cs 以及 Vertical Recyclable Grid Control.cs
- 在Panel层 或父级 ,加入一个Init的方法。即刷数据的方法即可。后续的滚动显示流程 已封装在RecyclableScrollView中,低耦合。
SetData方法
这是刷新数据的核心方法,需要给item身上的脚本继承RecyclableItem.cs
这里,主要是封装一些操作,然后规定一个setdata的流程
因为需要一开始获取item的位置信息,即transform,需要继承mono
using System;
using UnityEngine;
public abstract class RecyclableItem : MonoBehaviour
{
private Transform m_TransformCache = null;
public Transform itemTrans
{
get
{
if (m_TransformCache == null)
m_TransformCache = GetComponent<Transform>();
return m_TransformCache;
}
}
private int m_Index;
public int index
{
get => m_Index;
set => m_Index = value;
}
public Action<int> OnItemClickCb { get; set; }
protected virtual void Awake()
{
if (m_TransformCache == null)
m_TransformCache = GetComponent<Transform>();
}
public virtual void SetActive(bool active)
{
if (gameObject.activeSelf == active) return;
gameObject.SetActive(active);
}
public virtual void SetSelected(bool isSelected) {}
public abstract void SetData<T>(T data);
public virtual void ClearData() { }
public virtual void OnItemClicked()
{
OnItemClickCb?.Invoke(index);
}
}
拿法宝为例,override一个自己setdata的方法,因为需要从哪个缓存池拿数据,怎么填。这都是个体的过程
public class MagicWeaponListItem : RecyclableItem
{
//----其他方法-----//
public override void SetData<T>(T data)
{
var itemData = data as MagicWeaponItemData;
if (itemData != null)
{
try
{
switch (itemData.pageStatus)
{
case SHOWITEM_TYPE.OUTLOOK_CHOOSE_TYPE:
InitMagicWeaponItem(MagicOutlook.TalismanOutController()?.m_magicWeaponItemList[itemData.magicWeaponId],SHOWITEM_TYPE.OUTLOOK_CHOOSE_TYPE);
MagicOutlook.TalismanOutController()?.m_magicWeaponItemObjects.Add(this);
break;
case SHOWITEM_TYPE.FVELEMENT_JINGJIN:
InitMagicWeaponItemForFVElem(TalismanFVElementsLogic.Instance()?.m_magicWeaponItemList[itemData.magicWeaponId],SHOWITEM_TYPE.FVELEMENT_JINGJIN,GameManager.PlayerDataPool.TalismanList);
TalismanFVElementsLogic.Instance()?.m_magicWeaponItemObjects.Add(this);
break;
case SHOWITEM_TYPE.CHOOSE_MAGICWEAPON_TYPE:
InitMagicWeaponItem(AllMagicWeaponList.Instance()?.m_magicWeaponItemList[itemData.magicWeaponId],SHOWITEM_TYPE.CHOOSE_MAGICWEAPON_TYPE,false);
break;
}
SetCastResAct(false);
}
catch (Exception e)
{
Debug.LogWarning(e);
throw;
}
}
}
}
现在每个item初始化数据格式的方法有了。现在 整个循环列表用什么规则去初始化下面的item呢
这里就定义了一个controller.cs
RecyclableGridController.cs 它主要是持有子级的gird 在grid布局下 垂直/水平生成 item ,需要实例多少个item,以及上文说到 通过双端链表 去改变每个item的位置,来实现”滚动显示“都是它去做的事情。
这里给出主要方法
public abstract class RecyclableGridControl : MonoBehaviour
{
//base
public GameObject prefabItem; //实例的泛型
public RecyclableScrollView scrollView;//自定义的循环列表组件
public UIPanel scrollViewPanel;
public UIGrid grid;//布局
/// <summary>
/// 更新Item数据域位置
/// </summary>
protected virtual void UpdateItem(RecyclableItem item, int index)
{
item.index = index;
bool isNull = m_DataList == null || index < 0 || index >= m_DataList.Count;
item.SetData(isNull ? null : m_DataList[index]);//每个item 刷新数据在这里
item.SetActive(!isNull);
item.SetSelected(m_CurSelectedIndex == index);
UpdateItemPosition(item, index);
}
/// <summary>
/// 设定数据
/// </summary>
public void SetData<T>(List<T> list, bool resetPosition)
{
// 防止还没来得及初始化就设值的情况
TryInit();
m_DataList.Clear();
//赋值的数据
if (IsNullOrEmpty(list) == false)
{
for (int x = 0, count = list.Count; x < count; x++)
{
m_DataList.Add(list[x]);
}
}
//调整当前grid下item的位置
if (resetPosition)
{
ResetGrid();
return;
}
RefreshGrid();
}
}
RecyclableGridControll 的 SetData方法 (注意每个item自己也有一个setdata方法,两者是总体与 个体区别)就是循环列表更新数据的逻辑。
- **TryInit 阶段:**判定是否是第一次创建Grid,如果是,那么实例化几个prefab,是需要先去做的.其次就是 检查每个prefab的索引是否需要变化,【在移动到第2~6时,第一个的索引应该调整为7,即在grid下变成序号7的child为后续统一位置信息做铺垫】。
- 缓存需要加载的全部信息:全部子项数据从 list 里进入m_datalist缓存,为下一步赋值进行准备工作
- 赋值以及计算实时位置:按照Grid每个item 的编号 通过postion.y = (index -1) * item.height 更新真正位置。
- 完成数据项自己的初始化信息的过程,即updateItem 方法,此方法包含在ResetGrid流程的最后一步
至此,循环列表的制作就完成了。
可以看到右侧Grid下只创建了七个实例,但左侧可以显示理论上 无穷的信息。
性能分析
对比循环列表和普通列表,创建1000个子物体,具体实现方法如下:
第一次初始化的耗时,循环列表耗时约17ms,普通列表耗时约342ms,性能提升约20倍左右,如下:
第二次更新列表数据时,由于循环列表仅需修改数据,而普通列表需要先清空子物体再生成,耗时差距进一步拉大,循环列表耗时约0.3ms,普通列表耗时约672ms,性能提升约2000倍左右,如下