当前位置: 首页 > web >正文

Unity动态列表+UniTask异步数据请求

Unity动态列表+UniTask异步数据请求

很久没有写东西了。最近有一个需求,在Unity项目里,有几个比较长的列表,经历了一翻优化,趁这几日闲暇,记录下来,给自己留个笔记,也送给有缘之人共同探讨吧。
以其中一个列表为例,其大体需求是:首先向后台请求列表数据,后台会反馈一个列表,本质上是数据的id数组。然后,由于数据项很复杂,所以要知道某条信息的具体数据,还要以id为参数,再次向后台请求关于这条数据的具体数据。即:

  • 第一次只请求列表数据:
    Reuest:getCableList?key=filter
    Response:{ success: true, message: null, datas: [ 1, 2, 5, 10, 12 ] }
  • 第二次请求详细数据:
    Request: getCableData?id=5
    Response: { success: true, message: null, data: [{ name: “西线机房至汇聚光交箱”, descript: “Some Text” …},…]}

一、异步加载

现成的有UniTask和协程方案,学习了下前人总结的UniTask之后,感觉UniTask比协程好:第一,性能好,更少的GC;第二,更灵活,体现在能很方便的做取消、超时管理、提供更多的yield时机选择、而且还不需要MonoBehaviour;第三,写起来代码来更人性化;第四,免费,没有额外的代价。所以,UniTask确实很好。
第一个版本的代码如下,主要思路是:从后台请求列表数据,获取到列表之后,分批进行第二次请求,然后将数据加载到列表中,实例化Item并更新UI。

// 用POST方法请求文本数据
private static async UniTask<string> RequestTextWithPostMethod(string url, Dictionary<string, string> data, float waitTime=3f)
{try{CancellationTokenSource cts = new CancellationTokenSource();cts.CancelAfterSlim(TimeSpan.FromSeconds(waitTime));var request = await UnityWebRequest.Post(url, data).SendWebRequest().WithCancellation(cts.Token);return request.result == UnityWebRequest.Result.Success ? request.downloadHandler.text : null;}catch{return null;}
}private readonly ConcurrentDictionary<int, CableItem> activeItems = new();
// 更新列表数据
private void UpdateList(string key)
{try{// 向后台请求列表数据string json = await RequestTextWithPostMethod("http://demo.myhost.com/unity/getCableList",new Dictionary<string, string> { { "key", key } });if (string.IsNummOrEmpty(json))throw new Exception(serverErrorMessage);var res = JsonConvert.DeserializeObject<CableListResponse>(json);if (!res.success)throw new Exception(res.message);HashSet<int> ids = new(res.datas);// 如果已实例化的项不在请求结果中,则清除它们foreach (var aid in activeItems.Keys.Where(aid => !ids.Contains(aid))){itemPool.Release(activeItems[aid]);activeItems.TryRemove(aid, out _);}int allCount = res.datas.Count;int total = 0;// 每5个为一批,按批次异步请求数据,避免并发量太大foreach (var chunk in res.datas.Chunk(5)){var tasks = chunk.Select(async id =>{// 如果该ID未在活动列表中,则实例化该项if (!activeItems.TryGetValue(id, out CableItem item)){item = cableItemPool.Get();activeItems.TryAdd(id, item);item.transform.SetAsLastSibling();}// 发起第二次请求,将获取到的数据设置到Itemawait GetCableData(id).ContinueWith(cable =>{item.SetCableData(cable);});GlobalProgressBar.SetValue(0.1f + 0.9f * (++total / (float)allCount));});// 如果该批次已完成,则下一帧发起下一个批次await UniTask.WhenAll(tasks);await UniTask.Yield();}}catch (Exception e){errorMessageText.text = e.Message;}finally{isRequsting = false;}
}

经测试,上述代码确实挺好,在数据请求时,对帧率几乎没有影像。但是,它还是不够好,当列表非常大时,更新一次数据总体上还是需要很久,更要命的是,由于它列表项太多,使用原生的Scroll View组件会严重影像性能,当切换UI页面(需要关闭或激活ScrollView时操作明显有粘滞感)。想到的解决方案有二:

  • 其一,分页。每次之请求一部分数据,肯定能改善操作,但是需要后台也同步改为分页支持,而且需要增加上一页、下一页、页面展示等按钮还有逻辑,还会让操作更复杂,不太符合原需求。
  • 其二,优化ScrollView。必然选这个。

二、动态Scroll View

这并不是我的首创,早有各种大神实现过了。它思路很简单,Scroll View同时可视的Item是有限的,只需要保证能看见的Item处于激活态就好,其余的可以禁用掉。进一步优化下就是,保留可视列表项的前几个和后几个项激活,以便优化滚动。首先实现一个动态的超级ScrollView:

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Pool;
using UnityEngine.UI;namespace HXDynamicScrollView
{[RequireComponent(typeof(ScrollRect))]public abstract class DynamicScrollView<TData> : MonoBehaviour{[SerializeField] private DynamicScrollItem<TData> ItemPrefab;public float ItemHeight = 80f;		// 项的高度public float ItemSpacing = 10f;		// 项之间的间距public int PreheatCount = 10;		// 预加载可视列表附近的几个Itempublic TData[] DataArray { get; private set; }		// 数据列表public int totalItemCount => DataArray?.Length ?? 0;private ScrollRect m_scrollRect;private RectTransform m_viewport;private RectTransform m_content;private float contentHeight;private float ItemHeightWithSpcaing => ItemHeight + ItemSpacing;  // 每个项包括间距的高度private int currentFirstIndex = -1;private int currentLastIndex = -1;private readonly Dictionary<int, DynamicScrollItem<TData>> activeItems = new();  // 活动的项private ObjectPool<DynamicScrollItem<TData>> itemPool;	// Item对象池public delegate void OnItemInstancedHander(DynamicScrollItem<TData> item);public event OnItemInstancedHander OnItemInstanced;public delegate void OnItemActivedHandler(DynamicScrollItem<TData> item);public event OnItemActivedHandler OnItemActived;public delegate void OnItemRecycledHandler(DynamicScrollItem<TData> item);public event OnItemRecycledHandler OnItemRecycled;public delegate void OnBeforeItemDataChangedHander(TData[] datas);public event OnBeforeItemDataChangedHander OnBeforeItemDataChanged;private void Awake(){itemPool = new ObjectPool<DynamicScrollItem<TData>>(() =>{var item = Instantiate(ItemPrefab, m_content);OnItemInstanced?.Invoke(item);return item;},item =>{item.gameObject.SetActive(true);OnItemActived?.Invoke(item);},item =>{OnItemRecycled?.Invoke(item);item.gameObject.SetActive(false);},item => Destroy(item.gameObject));m_scrollRect = GetComponent<ScrollRect>();m_viewport = m_scrollRect.viewport;m_content = m_scrollRect.content;m_scrollRect.onValueChanged.AddListener(OnScrollViewChanged);m_content.anchorMin = new Vector2(0, 1);m_content.anchorMax = new Vector2(1, 1);m_content.pivot = new Vector2(0.5f, 1);m_content.sizeDelta = new Vector2(0, 0);}// 滚动条滚动事件private void OnScrollViewChanged(Vector2 _){UpdateVisibleItems();}// 设置数据项public void SetDataList(IEnumerable<TData> dataList){if(DataArray is {Length: > 0 })OnBeforeItemDataChanged?.Invoke(DataArray);DataArray = dataList.ToArray();CalculateContentHeight();UpdateVisibleItems(true);}// 计算内容高度private void CalculateContentHeight(){contentHeight = totalItemCount * (ItemHeight + ItemSpacing) + ItemSpacing;m_content.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, contentHeight);}// 更新可视的项private void UpdateVisibleItems(bool bForce = false){// 如果数据是空的,则清理现存的并直接返回if (DataArray is not { Length: > 0 }){foreach (var item in activeItems){item.Value.Hide();itemPool.Release(item.Value);}activeItems.Clear();return;}var viewportTop = m_content.anchoredPosition.y;var viewportBottom = viewportTop + m_viewport.rect.height;// 计算可视项前后预加载项的索引int newFirstIndex = Mathf.Max(0,Mathf.FloorToInt((viewportTop - PreheatCount * ItemHeightWithSpcaing) / ItemHeightWithSpcaing));int newLastIndex = Mathf.Min(totalItemCount - 1,Mathf.CeilToInt((viewportBottom + PreheatCount * ItemHeightWithSpcaing) / ItemHeightWithSpcaing));// 如果不需要更新则返回if (!bForce && currentFirstIndex == newFirstIndex && currentLastIndex == newLastIndex)return;// 清理需要删除的项List<int> toRemove = new();foreach (var item in activeItems.Where(item => item.Key < newFirstIndex || item.Key > newLastIndex)){item.Value.Hide();itemPool.Release(item.Value);toRemove.Add(item.Key);}foreach (var index in toRemove)activeItems.Remove(index);// 激活可视或可视附近的,即需要预加载的项for (int i = newFirstIndex; i <= newLastIndex; i++){if (!activeItems.ContainsKey(i)){var item = itemPool.Get();item.SetDataAndShow(DataArray[i]);PlaceItem(i, item);activeItems.Add(i, item);}}currentFirstIndex = newFirstIndex;currentLastIndex = newLastIndex;}// 放置项private void PlaceItem(int index, DynamicScrollItem<TData> item){float yPos = -index * ItemHeightWithSpcaing - ItemSpacing;item.anchoredPosition = new Vector2(0, yPos);}}
}
using UnityEngine;
using UnityEngine.EventSystems;namespace HXDynamicScrollView
{public abstract class DynamicScrollItem<TData> : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler{public virtual void Hide(){}public abstract void SetDataAndShow(TData data);public Vector2 anchoredPosition{get => ((RectTransform)transform).anchoredPosition;set=> ((RectTransform)transform).anchoredPosition = value;}protected bool IsMouseHover { get; private set; }public virtual void OnPointerEnter(PointerEventData eventData){IsMouseHover = true;}public virtual void OnPointerExit(PointerEventData eventData){IsMouseHover = false;}}
}

上面代码就是全部的超级ScrollView了。
以上面的CableList为例,使用它,变成异步动态加载的超级列表。每次滚动时,只会有非常少量的项被激活,所以无需分批,直接异步求情数据即可。

public class CableItem : DynamicScrollItem<int>
{// 这里省略了一些其他的代码public CableData Data { get; private set; }private void SetCableData(CableData data){Data = data;if (data != null){// 将数据显示到UI组件上}}// 当Item处于预加载或可视时,被调用,请求数据public override void SetDataAndShow(int data){UpdateData(data).Forget();}private async UniTaskVoid UpdateData(int id){var cableInfo = await GetCableData(id);if(cableInfo!=null)SetCableData(cableInfo);}private static async UniTask<CableData> GetCableData(int id){// 如果数据已存在,并且数据处于有效期内,则直接返回if (cables.TryGetValue(id, out CableData cable)){if (cable.IsEditing || Time.time - cable.lastUpdatetime < 300f)return cable;}// 向后台请求数据var json = await RequestTextWithPostMethod(urlGetCableInfo,new Dictionary<string, string> { { "id", id.ToString() } });try{if (string.IsNullOrEmpty(json))throw new Exception(serverErrorMessage);var cableData = JsonConvert.DeserializeObject<CableInfo>(json, JsonSettings);if (!cableData.success)throw new Exception(cableData.message);cableData.data.lastUpdatetime = Time.time;cableData.data.IsEditing = false;cables.AddOrUpdate(id, cableData.data, (_, _) => cableData.data);return cableData.data;}catch{return null;}}
}

上述Item还有进一步优化的空间,如,极端情况下,滚动速度很快,或网络情况不好的情况下,可能数据请求还未返回,Item就由可是状态变为非可视状态,此时,可以很容易的增加取消机制。在disable中进行取消即可。

结论

同时,项目还采取了其他的优化机制,比如,使用数据缓存,请求过的数据,在一定时间内再次使用时无需再次请求,还有使用对象池等奇数,经过上述优化后,项目中的列表非常丝滑,加载无感,进度条也删去了。

http://www.xdnf.cn/news/3390.html

相关文章:

  • 如何测试调用RagFlow的API功能
  • 《社交类应用开发:React Native与Flutter的抉择》
  • 【Java】HashMap
  • JGA811Ⅱ大气污染治理实训平台实验装置
  • Python学习笔记(第三部分)
  • (007)Excel 公式的使用
  • 【Machine Learning Q and AI 读书笔记】- 04 彩票假设
  • Linux系统中升级GNU Make构建工具版本至4.4.1
  • 深入解析Session与Cookie:从HTTP无状态到现代会话管理
  • 【树莓派Pico FreeRTOS】-FreeRTOS-SMP移植
  • MySQL事务隔离级别详解
  • 装饰器设计模式(Decorator Pattern)详解
  • React Redux 与 Zustand
  • Python10天冲刺-设计模型之策略模式
  • 定义一个3D cube,并计算cube每个顶点的像素坐标
  • Rust中避免过度使用锁导致性能问题的策略
  • 【音频】基础知识
  • Elasticsearch 根据两个字段搜索
  • Python项目源码69:Excel数据筛选器1.0(tkinter+sqlite3+pandas)
  • 约玩、搭子组局、线下约玩、助教系统源码
  • VSCode开发调试Python入门实践(Windows10)
  • HTTP知识速通
  • 计算机网络实验七:数据抓包与协议分析
  • 【STM32】ADC的认识和使用——以STM32F407为例
  • 分布式锁的几种实现
  • 使用HunyuanVideo搭建文本生视频大模型
  • OpenSSL应用实践:嵌入式数据安全实战指南
  • 使用Node编写轻量级后端快速入门
  • 极简GIT使用
  • 【内存管理】对象树(内存管理)