URLRequest请求完成前阻塞UI 放入后台队列仍卡顿问题求解
问题核心原因
你现有代码的卡顿和无效请求问题来自3个核心错误:
- 搜索回调运行在主线程,你在回调中用
Data(contentsOf: url)同步加载所有海报,同步网络请求会直接阻塞主线程,导致UI完全无响应 - 没有对旧搜索请求做取消逻辑,用户连续输入会产生大量无用请求,抢占网络和CPU资源,还可能导致旧请求结果覆盖新请求的问题
- 外层包的后台队列调用
store.searchMovies是完全无效的,URLSession.dataTask本身就是异步执行,不会阻塞当前队列,多余的GCD调用反而会增加不必要的线程调度开销
修复方案
按以下步骤修改即可解决问题:
- 给搜索接口增加旧请求取消逻辑,发起新搜索前终止上一次还在执行的请求
- 增加输入防抖,用户输入停止后间隔一段时间再发起搜索,避免频繁触发无效请求
- 把一次性加载所有海报的逻辑移除,改成UITableViewCell显示时异步加载对应海报,同时可增加缓存避免重复请求
- 所有UI相关操作确保放在主线程执行,非UI逻辑不要占用主线程资源
修改后代码示例
Store层搜索方法修改
首先在Store类中增加当前搜索任务的存储变量,用来取消旧请求:
private var currentSearchTask: URLSessionDataTask? func searchMovies(query: String, completion: @escaping (Result<[Movie], Error>) -> Void){ // 发起新请求前取消上一次未完成的搜索 currentSearchTask?.cancel() let url = TMDB_API.movieSearchURL(query: query) let request = URLRequest(url: url) let task = URLSession.shared.dataTask(with: request) { [weak self] (data, response, error) in // 忽略被取消的请求 if let error = error as? URLError, error.code == .cancelled { return } let result = self?.processMoviesRequest(data: data, error: error) ?? .failure(MovieError.requestCancelled) OperationQueue.main.addOperation { completion(result) } } currentSearchTask = task task.resume() }
搜索栏回调修改
在ViewController中增加防抖定时器,移除同步加载海报逻辑:
// 声明防抖定时器变量 private var searchDebounceTimer: Timer? func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { // 取消之前的待执行搜索任务 searchDebounceTimer?.invalidate() // 输入停止300ms后发起搜索,可根据业务调整间隔时长 searchDebounceTimer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { [weak self] _ in guard let self = self else { return } self.store.searchMovies(query: searchText) { moviesResult in switch moviesResult { case .success(let movies): // 直接更新数据源,不要在这里处理海报加载 MovieResults.movieResults.allResults = movies // 批量更新标识字段,避免额外遍历开销 MovieResults.movieResults.allResults.forEach { $0.isAddedToList = false } self.tableView.reloadData() case .failure(let error): // 忽略请求取消的报错 if let error = error as? URLError, error.code == .cancelled { return } print(error) } } } }
UITableViewCell异步加载海报
在cellForRowAt方法中处理单张海报的异步加载,避免一次性加载所有图片:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCell", for: indexPath) as! MovieCell let movie = MovieResults.movieResults.allResults[indexPath.row] // 先设置默认占位图,避免cell复用显示旧图片 cell.posterImageView.image = UIImage(data: genericPosterImageData) cell.titleLabel.text = movie.title cell.releaseDateLabel.text = movie.releaseDate guard let posterPath = movie.poster_path, let posterUrl = URL(string: "https://image.tmdb.org/t/p/w185/\(posterPath)") else { return cell } // 优先读取已缓存的海报数据,没有则发起网络请求 if let cachedThumb = movie.thumb { cell.posterImageView.image = UIImage(data: cachedThumb) return cell } // 异步加载海报 URLSession.shared.dataTask(with: posterUrl) { [weak cell, weak movie] data, _, error in guard let data = data, error == nil, let image = UIImage(data: data) else { return } DispatchQueue.main.async { cell?.posterImageView.image = image // 缓存海报数据,下次滑动到该位置不用重新请求 movie?.thumb = data } }.resume() return cell }
额外优化建议
- 可以将海报请求的尺寸从
original改为和cell匹配的小尺寸(比如示例中的w185),大幅提升加载速度、减少流量消耗 - 如果不想自己处理图片缓存、cell复用等逻辑,可以直接使用成熟的第三方图片加载库处理
- 全局数据源
MovieResults.movieResults的读写操作最好增加线程保护,避免多线程同时读写导致的崩溃问题
内容的提问来源于stack exchange,提问作者Luigi
相关产品推荐
相关产品推荐

