iOS进阶-剖析SDWebImage

概述

SDWebImage通过给UIView、UIImageView、UIButton、MKAnimation提供扩展,从而提供了异步下载图片、支持缓存的功能。

SDWebImage 架构

image.png

从上图可以看出SDWebImage有两个模块构成: UIKit + 工具类

用加载一张带展位图的例子来分析下,SDWebImage的工作流程,分两部分UIKit + 工具类

UIKit部分:

首先我们访问 UIWebImage+webCache

  1. 外部调用设置图片加上一张展位图
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder NS_REFINED_FOR_SWIFT;
  1. 都会调用UIWebImage+ webC的一下方法
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                   context:(nullable SDWebImageContext *)context
                  progress:(nullable SDImageLoaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock ;

这一步会调用UIView+webCache的 sd_internalSetImageWithURL 方法,可能会有疑问为什么UIImageview+webCache 直接就做了 ,考虑其他继承于UIView的控件需要加载图片,作者将加载方法封装到了 UIView

  1. UIView+webCache 的 sd_internalSetImageWithURL 这里关系不忠于理解逻辑的代码用 ...标示
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                           context:(nullable SDWebImageContext *)context
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
                         completed:(nullable SDInternalCompletionBlock)completedBlock {
 // ==============  UIView+ WebCache.m ============== //

    //valid key:UIImageView || UIButton
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    //UIView+WebCacheOperation 的 operationDictionary
    //下面这行代码是保证没有当前正在进行的异步下载操作, 使它不会与即将进行的操作发生冲突
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    

    //添加临时的占位图(在不延迟添加占位图的option下)
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    //如果url存在
    if (url) {
       
       ...
        __weak __typeof(self)wself = self;

       //SDWebImageManager下载图片
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
          
            ...
            //dispatch_main_sync_safe : 保证block能在主线程进行
            dispatch_main_async_safe(^{
                
                if (!sself) {
                    return;
                }
               
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                     //image,而且不自动替换 placeholder image
                    completedBlock(image, error, cacheType, url);
                    return;
                    
                } else if (image) {
                    //存在image,需要马上替换 placeholder image
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    [sself sd_setNeedsLayout];
                
                } else {                    
                    //没有image,在图片下载完之后显示 placeholder image
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        
        //在操作缓存字典(operationDictionary)里添加operation,表示当前的操作正在进行
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
        
    } else {
        //如果url不存在,就在completedBlock里传入error(url为空)
        dispatch_main_async_safe(^{
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
    ```



> 值得一提的是,在这一层,使用一个字典``operationDictionary``专门用作存储操作的缓存,随时添加,删除操作任务。
而这个字典是``UIView+WebCacheOperation``分类的关联对象,它的存取方法使用运行时来操作:


```objc
 // ==============  UIView+WebCacheOperation.m ============== //
 //获取关联对象:operations(用来存放操作的字典)
- (SDOperationsDictionary *)operationDictionary {
    SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
    //存放操作的字典
    if (operations) {
        return operations;
    }
    
    //如果没有,就新建一个
    operations = [NSMutableDictionary dictionary];
    
    objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return operations;
}

}

主要行为: 1. 如果有占位图片就展示出来 2. 处理化工具类的 SDWebImageManager ,并且调用 SDWebImageManager 的loadImageWithURL方法 3. 将当前加载图片的任务假如到一个任务缓存器保存,它的存储任务方法如下,使用runtime的关联对象实现。

 // ==============  UIView+WebCacheOperation.m ============== //
 //获取关联对象:operations(用来存放操作的字典)
- (SDOperationsDictionary *)operationDictionary {
    SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
    //存放操作的字典
    if (operations) {
        return operations;
    }
    
    //如果没有,就新建一个
    operations = [NSMutableDictionary dictionary];
    
    objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return operations;
}

到这里,UIKit层上面的东西都讲完了,现在开始正式讲解工具层。

工具类

上文提到过,SDWebImageManager同时管理SDImageCache和SDWebImageDownloader两个类,它是这一层的老大哥。在下载任务开始的时候,SDWebImageManager首先访问SDImageCache来查询是否存在缓存,如果有缓存,直接返回缓存的图片。如果没有缓存,就命令SDWebImageDownloader来下载图片,下载成功后,存入缓存,显示图片。以上是SDWebImageManager大致的工作流程。

  1. 接着上文继续 ,了解 SDWebImageManager 的loadImageWithURL 方法
 // ==============  SDWebImageManager.m ============== //
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock {
     ...                             
    //在SDImageCache里查询是否存在缓存的图片
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        
        ...
        //(没有缓存图片) || (即使有缓存图片,也需要更新缓存图片) || (代理没有响应imageManager:shouldDownloadImageForURL:消息,默认返回yes,需要下载图片)|| (imageManager:shouldDownloadImageForURL:返回yes,需要下载图片)
        if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            
            //1. 存在缓存图片 && 即使有缓存图片也要下载更新图片
            if (cachedImage && options & SDWebImageRefreshCached) {
                [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }


            // 2. 如果不存在缓存图片
            ...
            
            //开启下载器下载
            //subOperationToken 用来标记当前的下载任务,便于被取消
            SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                    // 1. 如果任务被取消,则什么都不做,避免和其他的completedBlock重复
                
                } else if (error) {
                    
                    //2. 如果有错误
                    //2.1 在completedBlock里传入error
                    [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];

                            //2.2 在错误url名单中添加当前的url
                    if (   error.code != NSURLErrorNotConnectedToInternet
                        && error.code != NSURLErrorCancelled
                        && error.code != NSURLErrorTimedOut
                        && error.code != NSURLErrorInternationalRoamingOff
                        && error.code != NSURLErrorDataNotAllowed
                        && error.code != NSURLErrorCannotFindHost
                        && error.code != NSURLErrorCannotConnectToHost) {
                        
                       @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {
                    
                    //3. 下载成功
                    //3.1 如果需要下载失败后重新下载,则将当前url从失败url名单里移除
                    if ((options & SDWebImageRetryFailed)) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    
                    //3.2 进行缓存
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

                   
                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                    
                        //(即使缓存存在,也要刷新图片) && 缓存图片 && 不存在下载后的图片:不做操作
                                           
                    } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        
          //(下载图片成功 && (没有动图||处理动图) && (下载之后,缓存之前处理图片)               dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                // pass nil if the image was transformed, so we can recalculate the data from the image
                                //缓存图片
                                [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            //将图片传入completedBlock
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {
                        
                        //(图片下载成功并结束)
                        if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                        }
                        
                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }

                     //如果完成,从当前运行的操作列表里移除当前操作
                if (finished) {
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];
            
            //取消的block
            operation.cancelBlock = ^{
            
                //取消当前的token
                [self.imageDownloader cancel:subOperationToken];
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                //从当前运行的操作列表里移除当前操作
                [self safelyRemoveOperationFromRunning:strongOperation];
            };
        
        } else if (cachedImage) {
            
            //存在缓存图片
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            
            //调用完成的block
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            
            //删去当前的的下载操作(线程安全)
            [self safelyRemoveOperationFromRunning:operation];
        
        } else {
            
            //没有缓存的图片,而且下载被代理终止了
            __strong __typeof(weakOperation) strongOperation = weakOperation;
           
            // 调用完成的block
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            
            //删去当前的下载操作
            [self safelyRemoveOperationFromRunning:operation];
        }
    }];

    return operation;                                                             
}


#pragma mark - Private

// Query cache process
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                 url:(nonnull NSURL *)url
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context
                            progress:(nullable SDImageLoaderProgressBlock)progressBlock
                           completed:(nullable SDInternalCompletionBlock)completedBlock {
    // Check whether we should query cache
    BOOL shouldQueryCache = (options & SDWebImageFromLoaderOnly) == 0;
    if (shouldQueryCache) {
        id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
        NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
        @weakify(operation);
        
        // 调用SDWebImageCache 的 queryImageForKey 去缓存里找
        operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
            @strongify(operation);
            if (!operation || operation.isCancelled) {
                [self safelyRemoveOperationFromRunning:operation];
                return;
            }
            // Continue download process
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
        }];
    } else {
        // Continue download process
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
    }
}


主要行为:

1. loadImageWithURL调用 callCacheProcessForOperation
2. callCacheProcessForOperation 调用SDWebImageCache 的 queryImageForKey 去缓存里找

5. SDWebImageCache 的 queryImageForKey

 // ==============  SDImageCache.m ============== //
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
   
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
        
    //================查看内存的缓存=================//
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    
    // 如果存在,直接调用block,将image,data,CaheType传进去
    if (image) {
    
        NSData *diskData = nil;
        
        //如果是gif,就拿到data,后面要传到doneBlock里。不是gif就传nil
        if ([image isGIF]) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
        
        //因为图片有缓存可供使用,所以不用实例化NSOperation,直接范围nil
        return nil;
    }

    //================查看磁盘的缓存=================//
    NSOperation *operation = [NSOperation new];
    
    //唯一的子线程:self.ioQueue
    dispatch_async(self.ioQueue, ^{
        
        if (operation.isCancelled) {
            // 在用之前就判断operation是否被取消了,作者考虑的非常严谨
            return;
        }

        @autoreleasepool {
            
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];
            
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                
                // cost 被用来计算缓存中所有对象的代价。当内存受限或者所有缓存对象的总代价超过了最大允许的值时,缓存会移除其中的一些对象。
                NSUInteger cost = SDCacheCostForImage(diskImage);
                
                //存入内存缓存中
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });

    return operation;
}

主要行为:

1.  先去缓存里查找 ,如果没有进入第二步;调用SDWebImageDownLoader的downloadImageWithURL方法

SDWebImageDownLoader的downloadImageWithURL方法

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    if (url == nil) {
        if (completedBlock) {
            NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
            completedBlock(nil, nil, error, YES);
        }
        return nil;
    }
    
    SD_LOCK(self.operationsLock);
    NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
    // There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
    if (!operation || operation.isFinished || operation.isCancelled) {
        operation = [self createDownloaderOperationWithUrl:url options:options context:context];
        if (!operation) {
            SD_UNLOCK(self.operationsLock);
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Downloader operation is nil"}];
                completedBlock(nil, nil, error, YES);
            }
            return nil;
        }
        @weakify(self);
        operation.completionBlock = ^{
            @strongify(self);
            if (!self) {
                return;
            }
            SD_LOCK(self.operationsLock);
            [self.URLOperations removeObjectForKey:url];
            SD_UNLOCK(self.operationsLock);
        };
        self.URLOperations[url] = operation;
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        [self.downloadQueue addOperation:operation];
    }
    else if (!operation.isExecuting) {
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        } else {
            operation.queuePriority = NSOperationQueuePriorityNormal;
        }
    }
    SD_UNLOCK(self.operationsLock);
    
    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    
    SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
    token.url = url;
    token.request = operation.request;
    token.downloadOperationCancelToken = downloadOperationCancelToken;
    token.downloader = self;
    
    return token;
}

// 

主要行为:

1.  进行下载操作,下载结果通过一个 SDWebImageDownloadToken保存的block进行回调
全部评论

相关推荐

nbdy:字太多了,写简历不是写自传,亮点难点技能点列出来就行,要简明扼要
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务