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.pswp.cn/diannao/81236.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/81236.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/81236.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

pandas讀取Excel數據(.xlsx和.xls)到treeview

對于.xls文件&#xff0c;xlrd可能更合適&#xff0c;但需要注意新版本的xlrd可能不支持xlsx&#xff0c;不過用戶可能同時需要處理兩種格式&#xff0c;所以可能需要結合openpyxl和xlrd&#xff1f;或者直接用pandas&#xff0c;因為它內部會處理這些依賴。 然后&#xff0c;…

2025年Jetpack Compose集成網絡請求庫的完整實施方案

Compose中集成網絡請求庫&#xff0c;網絡請求現在Retrofit是最流行的。 首先在Compose中如何進行網絡請求&#xff0c;而不僅僅是集成庫。因為Compose本身是UI框架&#xff0c;網絡請求其實還是通過ViewModel或者Repository來處理&#xff0c;然后通過狀態管理來更新UI。所以…

機器視覺開發-攝像頭掃描二維碼

以下是使用Python和OpenCV實現攝像頭掃描二維碼的最簡單示例&#xff1a; import cv2 from pyzbar import pyzbar# 打開攝像頭 cap cv2.VideoCapture(0)print("正在掃描二維碼... (按 q 鍵退出)")while True:# 讀取攝像頭幀ret, frame cap.read()if not ret:print…

Seata服務端回滾事務核心源碼解析

文章目錄 前言一、doGlobalRollback3.1、changeGlobalStatus3.2、doGlobalRollback 前言 本篇介紹Seata服務端接收到客戶端TM回滾請求&#xff0c;進行處理并且驅動所有的RM進行回滾的源碼。 一、doGlobalRollback doGlobalRollback是全局回滾的方法&#xff1a; ??首先依舊…

新聞客戶端案例的實現,使用axios獲取數據并渲染頁面,路由傳參(查詢參數,動態路由),使用keep-alive實現組件緩存

文章目錄 0.頁面要求1.功能要求2.開始路由配置2.1.嵌套二級路由如何配置?2.2.路由重定向,NotFound頁面,去除"#"號 3.實現底部導航欄的高亮效果4.渲染首頁:使用axios請求數據5.路由傳參5.1.回顧:查詢參數傳參或者動態路由傳參5.2.具體代碼 6.渲染詳情頁7.解決請求過程…

文件操作--文件包含漏洞

本文主要內容 腳本 ASP、PHP、JSP、ASPX、Python、Javaweb --# 各種包含函數 檢測 白盒 代碼審計 黑盒 漏掃工具、公開漏洞、手工看參數值及功能點 類型 本地包含 有限制、無限制 遠程包含 無限制、有限制…

ActiveMQ 性能優化與網絡配置實戰(二)

五、性能優化實戰 5.1 基礎配置調整 5.1.1 增加并發消費者 在 ActiveMQ 中&#xff0c;增加并發消費者是提高消息處理效率的重要手段之一。通過配置多個消費者并行處理消息&#xff0c;可以充分利用系統資源&#xff0c;加快消息的消費速度&#xff0c;從而提高系統的整體吞…

C++/SDL 進階游戲開發 —— 雙人塔防(代號:村莊保衛戰 17)

&#x1f381;個人主頁&#xff1a;工藤新一 &#x1f50d;系列專欄&#xff1a;C面向對象&#xff08;類和對象篇&#xff09; &#x1f31f;心中的天空之城&#xff0c;終會照亮我前方的路 &#x1f389;歡迎大家點贊&#x1f44d;評論&#x1f4dd;收藏?文章 文章目錄 二…

貪心算法精解(Java實現):從理論到實戰

一、貪心算法概述 貪心算法&#xff08;Greedy Algorithm&#xff09;是一種在每一步選擇中都采取當前狀態下最優決策的算法策略。它通過局部最優選擇來達到全局最優解&#xff0c;具有高效、簡潔的特點。 核心特點&#xff1a; 局部最優選擇&#xff1a;每一步都做出當前看…

深度學習框架:PyTorch使用教程 !!

文章目錄 一、PyTorch框架簡介 1.1 什么是PyTorch 1.2 PyTorch的優勢 二、從入門到精通的PyTorch使用教程 2.1 入門階段 2.1.1 環境安裝與配置 2.1.2 Tensor基礎操作 2.1.3 自動求導&#xff08;Autograd&#xff09; 2.1.4 構建神經網絡&#xff08;nn模塊&#xff09; 2.1.5 …

系統架構設計師:設計模式——創建型設計模式

一、創建型設計模式 創建型模式抽象了實例化過程&#xff0c;它們幫助一個系統獨立于如何創建、組合和表示它的那些對象。一個類創建型模式使用繼承改變被實例化的類&#xff0c;而一個對象創建型模式將實例化委托給另一個對象。 隨著系統演化得越來越依賴于對象復合而不是類…

Dinero.js - 免費開源的 JavaScript 貨幣處理工具庫,完美解決 JS 浮點數精度丟失問題

今天介紹一個在前后端處理貨幣的工具庫&#xff0c;logo 很可愛&#xff0c;是一只藍色的招財小貓。 本文封面圖底圖來自免費 AI 圖庫 StockCake。 Dinero.js 是一個用于貨幣計算的 JavaScript 工具庫&#xff0c;解決開發者在金融、電商、會計等場景中處理貨幣時的精度丟失、…

HNUST湖南科技大學-嵌入式考試選擇題題庫(109道糾正詳解版)

HNUST嵌入式選擇題題庫 1.下面哪點不是嵌入式操作系統的特點。(B) A.內核精簡 B.功能強大 C.專用性強 D.高實時性 解析&#xff1a; 嵌入式操作系統特點是內核精簡、專用性強、高實時性&#xff0c;而"功能強大"通常指的是通用操作系統&#x…

【工具】Windows批量文件復制教程:用BAT腳本自動化文件管理

一、引言 在日常開發與部署過程中&#xff0c;文件的自動化復制是一個非常常見的需求。無論是在構建過程、自動部署&#xff0c;還是備份任務中&#xff0c;開發者經常需要將某個目錄中的 DLL、配置文件、資源文件批量復制到目標位置。相比使用圖形界面的復制粘貼操作&#xf…

xray-poc編寫示例

禁止未授權掃描和測試行為&#xff01;&#xff01;&#xff01; 1. SQL 時間盲注檢測 (Time-Based Blind SQLi) name: generic/time-based-sqli rules:- method: GETpath: "/product?id1 AND (SELECT 1 FROM (SELECT SLEEP(5))a)--"expression: |response.status…

【Day 14】HarmonyOS分布式數據庫實戰

一、分布式數據庫基礎 1. 核心概念速記表 術語解釋示例場景分布式數據庫數據自動同步到同賬號設備手機添加商品→平板立即顯示KV數據模型鍵值對存儲&#xff08;類似JSON&#xff09;{"cart_item1": {"name":"牛奶","price":10}}數據…

【數據結構】- 棧

前言&#xff1a; 經過了幾個月的漫長歲月&#xff0c;回頭時年邁的小編發現&#xff0c;數據結構的內容還沒有寫博客&#xff0c;于是小編趕緊停下手頭的活動&#xff0c;補上博客以洗清身上的罪孽 目錄 前言&#xff1a; 棧的應用 括號匹配 逆波蘭表達式 數制轉換 棧的實…

TDA4VM SDK J721E (RTOS/Linux) bootloaders梳理筆記

文章目錄 1. 前言2. RTOS BootLoader2.1 引導模式2.2 啟動序列2.2.1 流程框圖2.2.2 Memory map2.3 鏡像格式詳解3. Linux BootLoader鏡像格式詳解啟動流程參考1. 前言 TDA4VM的BootLoader包含兩部分:RTOS的和Linux的。 2. RTOS BootLoader 這是在SoC上的所有內核運行FreeRTO…

Spring Boot + MyBatis-Plus 的現代開發模式

之前的Maven項目和本次需要的環境配置并不一樣 之前使用的是&#xff1a; 傳統的 MyBatis 框架&#xff08;非 Spring Boot 環境&#xff09; 手動管理 SqlSession 使用了 .xml 的 Mapper 映射文件 沒有 Spring 容器管理&#xff08;沒有 Service / RestController 等&…

【Quest開發】極簡版!透視環境下摳出身體并能遮擋身體上的服裝

前兩天發了一個很復雜的版本&#xff0c;又鼓搗了一下發現完全沒有必要。我之前的理解有點偏&#xff08;不是錯誤的但用法錯了&#xff09;&#xff0c;但是有一些小伙伴收藏了&#xff0c;害怕里面的某些東西對誰有用&#xff0c;所以寫了一篇新的&#xff0c;前兩步配置環境…