异步加载API数据至ListView时搜索卡顿的优化方案咨询
优化搜索时API请求卡顿的解决方案
我之前也遇到过类似的批量API请求导致UI卡顿的问题,结合你的代码场景,给你几个实用的优化方向,一步步解决这个问题:
1. 实现请求取消机制,终止无效的搜索请求
当用户快速输入搜索内容时,之前的API请求其实已经没有意义了,我们可以用CancellationToken来取消这些正在进行的请求,避免资源浪费。另外,建议把过时的WebClient换成HttpClient(微软官方推荐,更支持现代异步操作和取消)。
代码示例:
首先在ViewModel中维护一个CancellationTokenSource来管理搜索请求的生命周期:
private CancellationTokenSource _searchCancellationTokenSource; // 搜索文本变化时触发的方法 public async Task OnSearchTextChanged(string newSearchText) { // 取消之前未完成的请求 _searchCancellationTokenSource?.Cancel(); _searchCancellationTokenSource?.Dispose(); _searchCancellationTokenSource = new CancellationTokenSource(); // 节流:等待用户停止输入300ms再发起请求,避免每次输入字符都触发搜索 try { await Task.Delay(300, _searchCancellationTokenSource.Token); } catch (OperationCanceledException) { return; // 用户又输入了新内容,直接返回 } // 获取你的搜索结果列表 var searchResults = GetFilteredItems(newSearchText); foreach (var item in searchResults) { if (_searchCancellationTokenSource.Token.IsCancellationRequested) break; // 检测到取消信号,终止后续请求 try { // 调用带取消令牌的API方法 item.AvgPrice = await GetMarketStatAvgPriceAsync(item.UniqueName, item.Location, _searchCancellationTokenSource.Token); } catch (OperationCanceledException) { break; // 请求被取消,直接退出循环 } catch (Exception ex) { Debug.Print($"加载{item.UniqueName}失败:{ex.Message}"); item.AvgPrice = "-"; } } }
然后优化你的API请求方法,支持取消令牌:
// 全局复用HttpClient,避免频繁创建销毁 private static readonly HttpClient _httpClient = new HttpClient(); public static async Task<string> GetMarketStatAvgPriceAsync(string uniqueName, Location location, CancellationToken cancellationToken) { try { // 先检查缓存 var cachedItem = MarketStatChartItemList?.FirstOrDefault(i => i.UniqueName == uniqueName); if (cachedItem != null && cachedItem.LastUpdate > DateTime.Now.AddHours(-1)) { return GetLatestAvgPriceFromCache(cachedItem, location); } // 构造API请求URL var apiUrl = $"https://www.albion-online-data.com/api/v1/stats/charts/{FormattingUniqueNameForApi(uniqueName)}?date={DateTime.Now:MM-dd-yyyy}"; // 发送带取消令牌的请求 using var response = await _httpClient.GetAsync(apiUrl, cancellationToken); response.EnsureSuccessStatusCode(); // 确保请求成功 var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); // 反序列化数据 var chartData = JsonConvert.DeserializeObject<List<MarketStatChartResponse>>(responseContent); // 更新缓存 if (cachedItem == null) { cachedItem = new MarketStatChartItem { UniqueName = uniqueName, MarketStatChartResponse = chartData, LastUpdate = DateTime.Now }; MarketStatChartItemList?.Add(cachedItem); } else { cachedItem.LastUpdate = DateTime.Now; cachedItem.MarketStatChartResponse = chartData; } return GetLatestAvgPriceFromCache(cachedItem, location); } catch (OperationCanceledException) { throw; // 向上抛出取消异常,让调用者处理 } catch (Exception ex) { Debug.Print(ex.StackTrace); Debug.Print(ex.Message); return "-"; } } // 提取重复的缓存数据处理逻辑,减少代码冗余 private static string GetLatestAvgPriceFromCache(MarketStatChartItem cachedItem, Location location) { var locationData = cachedItem.MarketStatChartResponse?.FirstOrDefault(itm => itm.Location == Locations.GetName(location))?.Data; if (locationData?.TimeStamps == null || !locationData.TimeStamps.Any()) return "-"; var latestTimestamp = locationData.TimeStamps.Max(); var latestIndex = locationData.TimeStamps.FindIndex(t => t == latestTimestamp); return latestIndex != -1 ? locationData.PricesAvg[latestIndex].ToString("N", LanguageController.DefaultCultureInfo) : "-"; }
2. 批量请求API,减少网络调用次数
如果你的目标API支持批量查询(比如一次性传入多个uniqueName),这会是最有效的优化方式——把50+个请求合并成1个,大幅降低网络开销和延迟。
假设API支持用逗号分隔多个uniqueName,可以实现批量请求方法:
public async Task<Dictionary<string, string>> GetBatchAvgPricesAsync(IEnumerable<string> uniqueNames, Location location, CancellationToken cancellationToken) { var result = new Dictionary<string, string>(); var cachedNames = new List<string>(); var needFetchNames = new List<string>(); // 先分离缓存中已有的和需要请求的项 foreach (var name in uniqueNames) { var cachedItem = MarketStatChartItemList?.FirstOrDefault(i => i.UniqueName == name); if (cachedItem != null && cachedItem.LastUpdate > DateTime.Now.AddHours(-1)) { result[name] = GetLatestAvgPriceFromCache(cachedItem, location); cachedNames.Add(name); } else { needFetchNames.Add(name); } } if (!needFetchNames.Any()) return result; // 构造批量请求URL var formattedNames = string.Join(",", needFetchNames.Select(FormattingUniqueNameForApi)); var apiUrl = $"https://www.albion-online-data.com/api/v1/stats/charts?uniqueNames={formattedNames}&date={DateTime.Now:MM-dd-yyyy}"; using var response = await _httpClient.GetAsync(apiUrl, cancellationToken); response.EnsureSuccessStatusCode(); var responseContent = await response.Content.ReadAsStringAsync(cancellationToken); // 假设API返回的是键为uniqueName的字典,根据实际API调整反序列化逻辑 var batchData = JsonConvert.DeserializeObject<Dictionary<string, List<MarketStatChartResponse>>>(responseContent); foreach (var name in needFetchNames) { if (batchData.TryGetValue(name, out var chartData)) { var cachedItem = MarketStatChartItemList?.FirstOrDefault(i => i.UniqueName == name); if (cachedItem == null) { cachedItem = new MarketStatChartItem { UniqueName = name, MarketStatChartResponse = chartData, LastUpdate = DateTime.Now }; MarketStatChartItemList?.Add(cachedItem); } else { cachedItem.LastUpdate = DateTime.Now; cachedItem.MarketStatChartResponse = chartData; } result[name] = GetLatestAvgPriceFromCache(cachedItem, location); } else { result[name] = "-"; } } return result; }
3. UI层面优化,提升用户体验
- 虚拟列表加载:确保你的ListView使用
VirtualizingStackPanel(WPF默认支持),它只会加载可见区域的项,避免一次性渲染所有50+项,减少UI线程压力。 - 加载状态提示:在列表项中添加加载指示器(比如小转圈图标),让用户知道数据正在加载,而不是觉得UI卡顿。
- 优先级加载:先加载列表顶部的可见项,再异步加载下方的项,让用户先看到部分可用数据。
4. 缓存策略优化
- 滑动过期:可以把缓存的过期策略从“固定1小时”改成“最后访问后1小时过期”,常用的搜索项会保持缓存,减少重复请求。
- 缓存清理:当缓存的
MarketStatChartItemList数量超过阈值(比如200条),可以清理最久未使用的缓存项,避免内存占用过高。
这些优化组合起来,应该能彻底解决搜索卡顿的问题——取消无效请求避免资源浪费,批量请求减少网络开销,节流减少不必要的触发,UI优化提升用户感知。
内容的提问来源于stack exchange,提问作者Triky313
相关产品推荐
相关产品推荐

