基于免费NBA API的特定球队球员多页加载性能优化问询
优化NBA API球员加载性能的Swift方案
嘿,我看了你这段加载NBA球队球员的代码,核心问题就是串行请求所有分页导致的加载慢,再加上几个线程安全和回调逻辑的小坑。我整理了几个实用的优化点,直接给你改好的代码和思路:
核心问题分析
你现在的代码是用for循环一页一页发起请求,必须等前一页返回才会发下一页——35页的话,总耗时就是35倍单页请求时间,这肯定慢。另外还有两个隐患:
players数组在多个线程被修改,会有数据竞争的风险- 每次请求完成都调用
completion,调用方会收到35次回调,这显然不符合预期
优化后的完整代码
我把所有优化点整合到了下面的代码里,每一步都加了注释:
func loadPlayers(for team: Team, completion: @escaping (_ errors: [Error]?, _ players: [Player]) -> ()) { let rest = RestManager() // 配置请求头 for (key, value) in headers { rest.requestHttpHeaders.add(value: value, forKey: key) } // 先获取总页数:如果调用方没传pages,建议先请求第一页拿到meta里的total_pages // 这里我先默认用35,你可以改成动态获取的逻辑 let totalPages = 35 var players = [Player]() var errors = [Error]() let dispatchGroup = DispatchGroup() // 用并发队列处理解析和筛选,不占主队列 let processingQueue = DispatchQueue(label: "com.nba.player.processing", attributes: .concurrent) // 用串行队列保证players和errors的线程安全 let syncQueue = DispatchQueue(label: "com.nba.player.sync") // 并行发起所有分页请求 for page in 1...totalPages { dispatchGroup.enter() rest.makeRequest(withEndPoint: "players?per_page=100&page=\(page)", withHttpMethod: .get) { result in // 不管成功失败,都标记任务完成 defer { dispatchGroup.leave() } // 处理响应为空的情况 guard let response = result.response else { if let error = result.error { syncQueue.async { errors.append(error) } } return } // 处理非200状态码 guard response.httpStatusCode == 200 else { if let error = result.error { syncQueue.async { errors.append(error) } } return } // 处理数据为空的情况 guard let data = result.data else { let emptyDataError = NSError(domain: "NBAAPI", code: -1, userInfo: [NSLocalizedDescriptionKey: "Page \(page) returned no data"]) syncQueue.async { errors.append(emptyDataError) } return } // 后台解析JSON并筛选球员,不占用主队列 processingQueue.async { let decoder = JSONDecoder() guard let playerResult = try? decoder.decode(PlayerData.self, from: data) else { let decodeError = NSError(domain: "NBAAPI", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to decode page \(page) data"]) syncQueue.async { errors.append(decodeError) } return } // 筛选当前球队的球员 let teamPlayers = playerResult.data.filter { $0.team?.abbreviation == team.abbreviation } // 线程安全地添加到结果数组 syncQueue.async { players.append(contentsOf: teamPlayers) } } } } // 所有请求完成后,在主队列回调结果 dispatchGroup.notify(queue: .main) { let finalErrors = errors.isEmpty ? nil : errors completion(finalErrors, players) } }
关键优化点说明
- 并行请求所有分页:用
DispatchGroup管理所有请求,同时发起35页的请求,总耗时直接降到最慢的那一页的时间,这是提升速度的核心。 - 线程安全保障:用专门的串行队列
syncQueue来修改players和errors数组,避免多线程同时修改导致的数据混乱。 - 统一回调时机:只有当所有请求都完成后,才会调用一次
completion,符合调用方的预期。 - 后台处理解析和筛选:把JSON解析、球员筛选的逻辑放到后台队列,不占用主队列,避免卡UI。
- 完善的错误处理:收集所有失败页面的错误,而不是单个失败就终止整个流程,调用方可以知道哪些页面出了问题。
额外的进阶优化建议
- 动态获取总页数:不要硬编码35页,先发起一次请求(第一页),解析
meta.total_pages,这样API更新页数后代码也不用改。 - 添加请求取消能力:给方法加一个返回值(比如
[URLSessionTask]),或者用自定义的Cancelable类,方便用户离开页面时取消所有未完成的请求。 - 本地缓存:把筛选后的球员数据缓存到本地(比如UserDefaults、CoreData),下次用户查看同一球队时直接读缓存,不用重复请求API。
- 内存优化:如果担心3500条球员数据占内存,可以边解析边筛选,像代码里这样,不用把所有球员都存到内存里。
内容的提问来源于stack exchange,提问作者Mat
相关产品推荐
相关产品推荐

