• SDWebImage4.0.0 源码解析


    在开发iOS的客户端应用时,经常需要从服务器下载图片,虽然系统提供了下载工具:NSData、NSURLSession等等方法,但是考虑到图片下载过程中,需要考虑的因素比较多,比如:异步下载、图片缓存、错误处理、编码解码等,以及实际需要中根据不同网络加载不同画质的图片等等需求,因此下载操作不是一个简单的下载动作就可以解决。

    针对上述问题,目前常用的开源库就是SDWebImage,它很好的解决了图片的异步下载、图片缓存、错误处理等问题,得到了广泛的应用,使得设置UIImageViewUIButton对象的图片十分方便。本文就从源码的角度,剖析一下这款优秀的开源库的具体实现。

    类结构图

    SDWebImage的源码的类结构图和下载流程图在官方的说明文档里有介绍,通过UML类结构图详细的介绍了该框架的内部结构,以及通过流程图介绍了具体的下载过程。

    下图是我总结的SDWebImage的结构图,简单的把SDWebImage源码文件按照功能进行了划分,方便在阅读源码时,能快速的对源码有一个总体的认识,加快阅读效率。

    ![](//upload-images.jianshu.io/upload_images/1843940-c51585b28704fae9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

    关键类功能简介:

    • 1.下载类

    SDWebImageDownloader:提供下载的方法给SDWebImageManager使用,提供了最大并发量的下载控制、超时时间、取消下载、下载挂起、是否解压图片等等功能。同时,还提供了开始下载和停止下载的通知,给使用者监测下载状态,如果使用者不用监测下载状态,就不用监测该通知,这种设计模式很灵活,给使用者提供了更方便的选择。

    extern NSString * _Nonnull const SDWebImageDownloadStartNotification;
    extern NSString * _Nonnull const SDWebImageDownloadStopNotification;
    

    SDWebImageDownloaderOperation:继承自NSOperation,是图片下载的具体实现类,通过加入到NSOperationQueue中,然后在start方法中来开启下载操作。

    • 2.图片缓存

    SDImageCacheConfig:主要提供缓存的配置信息,如:是否解压图片、是否缓存到内存、最大缓存时间(默认是一周)和最大缓存的字节数等等。

    SDImageCache:缓存实现类,提供最大缓存字节、最大缓存条目的控制,以及缓存到内存及磁盘、从内存或磁盘删除、查询检索和查询缓存信息等功能。

    • 3.分类

    UIImageView+WebCacheUIImageView的分类,提供了设置UIImageView对象图片的多种方法,下面的方法可以说是SDWebImage框架中最常用的方法。

    - (void)sd_setImageWithURL:(nullable NSURL *)url
              placeholderImage:(nullable UIImage *)placeholder
                       options:(SDWebImageOptions)options;
    
    // 带完成block的赋值方法                   
    - (void)sd_setImageWithURL:(nullable NSURL *)url
              placeholderImage:(nullable UIImage *)placeholder
                       options:(SDWebImageOptions)options
                     completed:(nullable SDExternalCompletionBlock)completedBlock;
    

    UIButton+WebCacheUIButton的分类,提供了设置按钮图片和按钮背景图片的功能

    - (void)sd_setImageWithURL:(nullable NSURL *)url
                      forState:(UIControlState)state
              placeholderImage:(nullable UIImage *)placeholder;
    
    • 4.工具类

    SDWebImageDecoder:图像解码的工具类,通过imageNames:加载图片会立即进行解码,而通过imageWithContentsOfFile:则不会

    SDWebImagePrefetcher:批量图像下载工具,针对UI界面中需要下载多个图片时,又要在滑动中保持流畅体验,则可以使用该工具类批量下载图片,然后在给具体的UI控件设置图片时,就会直接从缓存中取

    SDWebImageManager:下载管理类工具,是SDWebImage的核心类,从官方文档的类图中也可以看出,提供了查看图片是否已经缓存、下载图片、缓存图片、取消所有的下载等等功能

    • 5.图片格式类

    NSData+ImageContentType:根据图片数据的第一个字节来获取图片的格式,可以区分PNGJPEGGIFTIFFWebP

    以上只是对SDWebImage类结构图的简单分析,如果需要进一步了解各个类的具体实现,请参考文末的资料,已有人详细的介绍了各个类的功能实现原理或方法。

    应用

    下面介绍一个在应用SDWebImage设置UI图片的源码实现过程

    在UIImageView上的应用

    设置图片

    通过设置URL、占位图片、图片配置、图片下载进度回调和设置完成回调来给UIImageView对象设置图片

    // ViewController.m
    [self.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://rescdn.qqmail.com/dyimg/20140302/73EB27F4A350.jpg"] placeholderImage:[UIImage imageNamed:@"gift-icon"] options:0 progress:nil completed:nil];
    

    上述代码调用UIImageView+WebCache.m里的方法

    - (void)sd_setImageWithURL:(nullable NSURL *)url
              placeholderImage:(nullable UIImage *)placeholder
                       options:(SDWebImageOptions)options
                      progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                     completed:(nullable SDExternalCompletionBlock)completedBlock {
        [self sd_internalSetImageWithURL:url
                        placeholderImage:placeholder
                                 options:options
                            operationKey:nil
                           setImageBlock:nil
                                progress:progressBlock
                               completed:completedBlock];
    }
    

    然后调用UIView+WebCache.m中的方法获取图片,然后根据option的类型进行不同的设置

    // UIView+WebCache.m
    - (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                      placeholderImage:(nullable UIImage *)placeholder
                               options:(SDWebImageOptions)options
                          operationKey:(nullable NSString *)operationKey
                         setImageBlock:(nullable SDSetImageBlock)setImageBlock
                              progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                             completed:(nullable SDExternalCompletionBlock)completedBlock 
    {
        ...
        if (url) {
            ...
            
            __weak __typeof(self)wself = self;
            // 开始加载图片
            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_async_safe(^{
                    if (!sself) {
                        return;
                    }
                    if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                    	// 把图片放到completedBlock里处理,一般是手动设置图片,因为这样可以对图片做进一步处理
                        completedBlock(image, error, cacheType, url);
                        return;
                    } else if (image) {
                        // 设置图片
                        [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    } else {
                    	// 延迟加载占位图(获取图片之后)
                        if ((options & SDWebImageDelayPlaceholder)) {
                            [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                            [sself sd_setNeedsLayout];
                        }
                    }
                    
                    // 回调完成block,如果是nil,则不调用
                    if (completedBlock && finished) {
                        completedBlock(image, error, cacheType, url);
                    }
                });
            }];
        } else {
        	 // 处理url为nil的情况
            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);
                }
            });
        }
    }
    

    加载图片的具体实现代码在SDWebImageManager里面,先从缓存中取图片,如果缓存中没有图片,就从网络下载,然后设置图片,最后再缓存该图片

    // SDWebImageManager.m
    - (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(nullable SDInternalCompletionBlock)completedBlock 
    {
        ...
        // 从缓存中取图片
        operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
            if (operation.isCancelled) {
                // 如果操作被取消,就从runningOperations操作数组从把该操作删除
                [self safelyRemoveOperationFromRunning:operation];
                return;
            }
    
            if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
                if (cachedImage && options & SDWebImageRefreshCached) {
                    // 如果options设置为更新缓存,那么就需要从服务器从新下载图片,然后更新本地缓存
                    [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
                }
                ...
                // 创建下载器,从服务器下载图片
                SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                    __strong __typeof(weakOperation) strongOperation = weakOperation;
                    ...
                    else {
                        // 设置了options为失败了重试,则会把失败的url加入failedURLs数组
                        if ((options & SDWebImageRetryFailed)) {
                            @synchronized (self.failedURLs) {
                                [self.failedURLs removeObject:url];
                            }
                        }
                        
                        ...
                        } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                            // 对图片进行transform操作
                            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];
                                }
                                
                                [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];
                            }
                            // 回调完成的block
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        }
                    }
    
                    if (finished) {
                        // 下载完成,就从runningOperations数组中删除操作
                        [self safelyRemoveOperationFromRunning:strongOperation];
                    }
                }];
                // 设置取消下载的回调
                operation.cancelBlock = ^{
                    [self.imageDownloader cancel:subOperationToken];
                    __strong __typeof(weakOperation) strongOperation = weakOperation;
                    [self safelyRemoveOperationFromRunning:strongOperation];
                };
            } else if (cachedImage) {
                // 从缓存在取到图片,回调完成block
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
                [self safelyRemoveOperationFromRunning:operation];
            }
            ...
        }];
    
        return operation;
    }
    

    从缓存中取图片,是先从内存中取,如果在内存中取到,就在当前线程中直接回调doneBlock;如果内存中没有,就开子线程从磁盘中取,如果取到图片,就回调doneBlock

    // SDImageCache.m
    - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
        ...
        // First check the in-memory cache...
        UIImage *image = [self imageFromMemoryCacheForKey:key];
        if (image) {
            NSData *diskData = nil;
            if ([image isGIF]) {
                diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            }
            if (doneBlock) {
                doneBlock(image, diskData, SDImageCacheTypeMemory);
            }
            return nil;
        }
    
        NSOperation *operation = [NSOperation new];
        dispatch_async(self.ioQueue, ^{
            if (operation.isCancelled) {
                // do not call the completion if cancelled
                return;
            }
    
            @autoreleasepool {
                // 从磁盘中取图片的data
                NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
                // 从磁盘中直接取图片
                UIImage *diskImage = [self diskImageForKey:key];
                if (diskImage && self.config.shouldCacheImagesInMemory) {
                    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;
    }
    

    图片的下载过程是在SDWebImageDownloader.m中进行的,实质是通过SDWebImageDownloaderOperation(继承自NSOperation)对象,把该对象加入到downloadQueue里,然后在start方法里通过NSURLSession来下载图片。(其中,NSOperation有两个方法:mainstart,如果想使用同步,那么最简单方法的就是把逻辑写在main()中,使用异步,需要把逻辑写到start()中,然后加入到队列之中)

    // SDWebImageDownloader.m
    - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                       options:(SDWebImageDownloaderOptions)options
                                                      progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                     completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
        __weak SDWebImageDownloader *wself = self;
    
        return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
            __strong __typeof (wself) sself = wself;
            NSTimeInterval timeoutInterval = sself.downloadTimeout;
            // 设置超时时间为15s
            if (timeoutInterval == 0.0) {
                timeoutInterval = 15.0;
            }
    
            // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
            NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
            request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
            request.HTTPShouldUsePipelining = YES;
            if (sself.headersFilter) {
                request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
            }
            else {
                request.allHTTPHeaderFields = sself.HTTPHeaders;
            }
            SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
            operation.shouldDecompressImages = sself.shouldDecompressImages;
            
            ...
            // 加入操作队列,开始下载
            [sself.downloadQueue addOperation:operation];
            ...
    
            return operation;
        }];
    }
    

    SDWebImageDownloaderOperation对象加入到操作队列,就开始调用该对象的start方法。

    // SDWebImageDownloaderOperation.m
    - (void)start {
        // 如果操作被取消,就reset设置
        @synchronized (self) {
            if (self.isCancelled) {
                self.finished = YES;
                [self reset];
                return;
            }
    
            ...
            NSURLSession *session = self.unownedSession;
            if (!self.unownedSession) {
                // 创建session的配置
                NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
                sessionConfig.timeoutIntervalForRequest = 15;
                
                // 创建session对象
                self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                                  delegate:self
                                                             delegateQueue:nil];
                session = self.ownedSession;
            }
            
            self.dataTask = [session dataTaskWithRequest:self.request];
            self.executing = YES;
        }
        // 开始下载任务
        [self.dataTask resume];
    
        if (self.dataTask) {
            for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
                progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
            });
        } else {
            // 创建任务失败
            [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
        }
    
        ...
    }
    

    在下载过程中,会涉及鉴权、响应的statusCode判断(404304等等),以及收到数据后的进度回调等等,在最后的didCompleteWithError里做最后的处理,然后回调完成的block,下面仅分析一下didCompleteWithError的方法

    - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
        
        ...
        if (error) {
            [self callCompletionBlocksWithError:error];
        } else {
            if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
                /**
                 *  See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash
                 *  Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response
                 *    and images for which responseFromCached is YES (only the ones that cannot be cached).
                 *  Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication
                 */
                if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
                    // 如果options是忽略缓存,而图片又是从缓存中取的,就给回调传入nil
                    [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
                } else if (self.imageData) {
                    UIImage *image = [UIImage sd_imageWithData:self.imageData];
                    // 缓存图片
                    NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                    // 跳转图片的大小
                    image = [self scaledImageForKey:key image:image];
                    
                    // Do not force decoding animated GIFs
                    if (!image.images) {
                        // 不是Gif图像
                        if (self.shouldDecompressImages) {
                            if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
    #if SD_UIKIT || SD_WATCH
                                image = [UIImage decodedAndScaledDownImageWithImage:image];
                                [self.imageData setData:UIImagePNGRepresentation(image)];
    #endif
                            } else {
                                image = [UIImage decodedImageWithImage:image];
                            }
                        }
                    }
                    if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                        // 下载是图片大小的0
                        [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
                    } else {
                        // 把下载的图片作为参数回调
                        [self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
                    }
                } else {
                    [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
                }
            }
        }
        ...
    }
    

    以上就是给UIImageView对象设置图片的过程,可以看出还是比较复杂的,考虑的情况也比较多,不得不佩服作者的编码能力。至于UIButton的图片设置过程,分析情况类似,在此不做分析。

    SDWebImage的源码中在设置图片的过程中,还应用了多种技术:GCD的线程组、锁机制、并发控制、队列、图像解码、缓存控制等等,是一个综合性十分强的项目了,通过阅读源码,对这些技术的使用也有了进一步的认知,对作者的编程功力的深厚深深折服。


    SDWebImage的解析到此结束,本文只是简单的从源码结构、UIImageView的使用角度进行了简单的分析,希望对阅读源码的朋友有一些帮助,如果文中有不足之处,还望不吝指出,互相学习。

    参考资料

    SDWebImage源码

    SDWebImage源码解读

    SDWebImage源码(一)——SDWebImage概览

    iOS开发——你真的会用SDWebImage?

  • 相关阅读:
    Asp.Net Core 2.0 项目实战(11) 基于OnActionExecuting全局过滤器,页面操作权限过滤控制到按钮级 郑州
    NET项目反编译+VS解决方案整理流程 郑州
    iis 目录枚举文件枚举解决方案 郑州
    Asp.Net Core 2.0 项目实战(10) 基于cookie登录授权认证并实现前台会员、后台管理员同时登录 郑州
    SqlServer mssql 按月统计所有部门 郑州
    无法获得数据库 'model' 上的排他锁 解决方法 郑州
    Asp.Net Core 2.0 项目实战(5)Memcached踩坑,基于EnyimMemcachedCore整理MemcachedHelper帮助类。 郑州
    谈谈前端怎样布局切图,程序可以方便快捷添加程序 郑州
    各种UIColor颜色的设置
    iOS
  • 原文地址:https://www.cnblogs.com/fishbay/p/7216164.html
Copyright © 2020-2023  润新知