当一个tableview中许多cell中的uiimageview请求相同地址的图片时,如何保证对于同一url只进行一次网络请求,从而避免没必要的网络请求以提高运行效率。
对于这个问题,如果对于同一url请求,当任何一次请求没完成保存本地缓存时,其他请求先去查看本地缓存,这个时候是找不到的,所以也会发送网络请求,这样确实存在效率问题。
自己设计的图片请求如何解决这个问题,首先想到的是使用字典以url为key,每次网络请求之前查找key,如果没有则请求网络下载,如果有则不请求,这样有一个问题急待解决,就是没有请求如何设置image并且在合适的时机设置image呢?想到的笨方法当然是当第一个请求完成并设置了本地缓存时通知另外的uiimage去再掉一次设置image方法 这个时候会走缓存获取效率非常高,这个方法还得继承uiimageview,在新的uiimageview中响应通知,显然这种方式不优秀。查阅资料了解到第三方图片加载库是如何处理的呢。
大致思路就是,维护一个字典,以URL为key,value是相同url对应的回掉block数组,如果是第一次请求时则在字典中添加key-value,第二次有相同url的请求时,通过key把value数组取出来,数组中添加新的callbackBlock,党请求结束时取出callback数组遍历执行,然后把key-value删除。
swift图片框架KingFisher的处理:
//KingFisher处理相同URL未返回时多次请求 let downloadTask: DownloadTask if let existingTask = sessionDelegate.task(for: url) {
//Url作为key,如果已存在相同url的任务,取出请求任务 downloadTask = sessionDelegate.append(existingTask, url: url, callback: callback) } else { let sessionDataTask = session.dataTask(with: request) sessionDataTask.priority = options.downloadPriority downloadTask = sessionDelegate.add(sessionDataTask, url: url, callback: callback) }
//根据url取出请求任务
func task(for url: URL) -> SessionDataTask? { lock.lock() defer { lock.unlock() } return tasks[url] }
//第一请求时,没有任务添加新请求任务
func add( _ dataTask: URLSessionDataTask, url: URL, callback: SessionDataTask.TaskCallback) -> DownloadTask { lock.lock() defer { lock.unlock() } // Create a new task if necessary. let task = SessionDataTask(task: dataTask) task.onCallbackCancelled.delegate(on: self) { [unowned task] (self, value) in let (token, callback) = value let error = KingfisherError.requestError(reason: .taskCancelled(task: task, token: token)) task.onTaskDone.call((.failure(error), [callback])) // No other callbacks waiting, we can clear the task now. if !task.containsCallbacks { let dataTask = task.task self.remove(dataTask) } } let token = task.addCallback(callback) tasks[url] = task return DownloadTask(sessionTask: task, cancelToken: token) }
如果请求未开始则执行请求任务
// Start the session task if not started yet. if !sessionTask.started { sessionTask.onTaskDone.delegate(on: self) { (self, done) in // Underlying downloading finishes. // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback] let (result, callbacks) = done // Before processing the downloaded data. do { let value = try result.get() self.delegate?.imageDownloader( self, didFinishDownloadingImageForURL: url, with: value.1, error: nil ) } catch { self.delegate?.imageDownloader( self, didFinishDownloadingImageForURL: url, with: nil, error: error ) } switch result { // Download finished. Now process the data to an image. case .success(let (data, response)): let processor = ImageDataProcessor( data: data, callbacks: callbacks, processingQueue: options.processingQueue) processor.onImageProcessed.delegate(on: self) { (self, result) in // `onImageProcessed` will be called for `callbacks.count` times, with each // `SessionDataTask.TaskCallback` as the input parameter. // result: Result<Image>, callback: SessionDataTask.TaskCallback let (result, callback) = result if let image = try? result.get() { self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response) } let imageResult = result.map { ImageLoadingResult(image: $0, url: url, originalData: data) } let queue = callback.options.callbackQueue queue.execute { callback.onCompleted?.call(imageResult) } } processor.process()//在这里边遍历执行对应URl的所有callback case .failure(let error):
callbacks.forEach { callback in let queue = callback.options.callbackQueue queue.execute { callback.onCompleted?.call(.failure(error)) } } } } delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request) sessionTask.resume() }
具体的遍历执行对应URl的所有callback
func process() { queue.execute(doProcess) } private func doProcess() { var processedImages = [String: Image]() for callback in callbacks { let processor = callback.options.processor var image = processedImages[processor.identifier] if image == nil { image = processor.process(item: .data(data), options: callback.options) processedImages[processor.identifier] = image } let result: Result<Image, KingfisherError> if let image = image { var finalImage = image if let imageModifier = callback.options.imageModifier { finalImage = imageModifier.modify(image) } if callback.options.backgroundDecode { finalImage = finalImage.kf.decoded } result = .success(finalImage) } else { let error = KingfisherError.processorError( reason: .processingFailed(processor: processor, item: .data(data))) result = .failure(error) } onImageProcessed.call((result, callback)) } }