SDWebImage简单解析

SDWebImage

一个支持远程服务器图片加载缓存的库

功能简介

  • UIImageViewUIButtonMKAnnotationView添加Web图像和缓存管理的类别
  • 一个异步图片下载器
  • 一个异步内存磁盘图片缓存且自动处理过期图片
  • 背景图片压缩
  • 保证同一个URL不会被多次下载
  • 保证不会一次又一次地重试伪造的URL
  • 保证主线程永远不会被阻塞
  • 性能!
  • 使用GCD和ARC

工作流程

  1. 入口 sd_setImageWithURL:placeholderImage:options:progress:completed: 会先取消上次的加载操作,再设置 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。
  2. 进入 **SDWebImageManager ** 中的loadImageWithURL:options:progress:completed:,交给 SDImageCache 从缓存中查找图片
  3. 先从内存缓存查找是否有图片 imageFromMemoryCacheForKey:,如果内存中已经有图片缓存,直接调用 SDCacheQueryCompletedBlock
  4. SDWebImageManager 回调 SDInternalCompletionBlockUIView+WebCache 等前端展示图片。
  5. 如果内存缓存中没有,生成 queryDiskBlock 添加到队列中开始从硬盘查找图片。
  6. 根据哈希之后的 URL Key 在磁盘缓存目录下查找图片,这一步是根据 SDImageCacheOptions 决定同步查找还是异步在 ioQueue 队列中查找,查找完成后将图片添加到内存缓存中,然后异步回到主线程中再返回图片给 SDWebImageManager
  7. 如果缓存中获取不到图片,则通过 SDWebImageDownloader 下载图片。
  8. 如果该URL已存在下载操作 NSOperation operation (默认为 SDWebImageDownloaderOperation 类型),则将当前所对应的 progressBlockcompletedBlock 添加到该 operationcallbackBlocks 数组中,图片下载由 **NSURLSession ** 来做。
  9. SDWebImageDownloaderOperation 中的 URLSession:dataTask:didReceiveData: 中实现边下载边解码图片
  10. 下载完图片之后,遍历 callbackBlocks 数组中的所有完成回调操作,将下载到的二进制数据和图片返回给 SDWebImageManagerSDWebImageManager 将图片添加到缓存中。

源码分析

Cache

减少网络请求次数,节省流量,下载完图片后存储到本地,下载再获取同一个URL时,优先从本地获取,提升用户体验。
SDWebImage 对图片进行缓存工作主要由 SDImageCache 完成。主要用于处理内存缓存和磁盘缓存,其中磁盘缓存的写操作是异步的,不会对UI造成影响。

内存缓存

内存缓存采用的是 NSCache + NSMapTable 双重缓存机制,SDMemoryCache 继承于 NSCache, 会自动处理内存缓存问题,并在收到内存警告的时候,移除自身所缓存的内存资源。但是SDMemoryCache 中的 weakCache 并不会在收到内存警告的时候清除。

self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];

weakCache 中存储的 key 值是对URLKey 的强引用,而 value 则是对 UIImage 的弱引用,并不会额外占用内存资源。

磁盘缓存

磁盘缓存的处理通过 NSFileManager 对象实现,图片存储的位置位于 cache 文件夹,还可以设置 customPaths 数组来自定义磁盘查询目录。另外 SDImageCache 中还有 ioQueue 串行队列来异步查询存储图片。

存图片

存储图片API:

- (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key completion:(nullable SDWebImageNoParamsBlock)completionBlock;
- (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock;
- (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock;
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key;

先保存到内存缓存中,同时保存对这张图片的一个弱引用

/// SDMemoryCache
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
    [super setObject:obj forKey:key cost:g];
    if (!self.config.shouldUseWeakMemoryCache) {
        return;
    }
    if (key && obj) {
        // Store weak cache
        LOCK(self.weakCacheLock);
        // Do the real copy of the key and only let NSMapTable manage the key's lifetime
        // Fixes issue #2507 https://github.com/SDWebImage/SDWebImage/issues/2507
        [self.weakCache setObject:obj forKey:[[key mutableCopy] copy]];
        UNLOCK(self.weakCacheLock);
    }
}

接着异步缓存图片到磁盘中,根据图片类型,通过 SDWebImageCodersManager 将图片解码为 NSData 类型,将图片资源保存到默认的缓存目录中,文件名为对 key 进行 MD5 后的值。

查图片

查询图片API:

- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key;    // 内存缓存中查
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key;         // 磁盘缓存中查

SDImageCache 中查图片:

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    
    // First check the in-memory cache...
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
    
    NSOperation *operation = [NSOperation new];
    void(^queryDiskBlock)(void) =  ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }
        
        @autoreleasepool {
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeNone;
            if (image) {
                // the image is from in-memory cache
                diskImage = image;
                cacheType = SDImageCacheTypeMemory;
            } else if (diskData) {
                cacheType = SDImageCacheTypeDisk;
                // decode image data only if in-memory cache missed
                diskImage = [self diskImageForKey:key data:diskData options:options];
                if (diskImage && self.config.shouldCacheImagesInMemory) {
                    NSUInteger cost = diskImage.sd_memoryCost;
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
            }
            
            if (doneBlock) {
                if (options & SDImageCacheQueryDiskSync) {
                    doneBlock(diskImage, diskData, cacheType);
                } else {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        doneBlock(diskImage, diskData, cacheType);
                    });
                }
            }
        }
    };
    
    if (options & SDImageCacheQueryDiskSync) {
        queryDiskBlock();
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    
    return operation;
}

优先从内存缓存中查找图片,默认内存中查到后不会从磁盘中查,内存查不到缓存,则从默认缓存目录和自定义的查找目录 customPaths 中遍历查找。

删图片

删除图片API:

- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;

同步从内存缓存中删除图片,同时对图片的弱引用也会删除,磁盘图片则是异步删除,磁盘图片资源只会从默认缓存目录中删除,而不会删除 customPaths 中的图片资源。

清缓存

清除缓存API:

- (void)clearMemory;
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;

删除磁盘缓存是异步删除。

在 iOS 应用或 TV 应用上,对于一些过期失效的磁盘资源,SDImageCache 会在合适的时机去清除:

  1. APP 即将销毁
  2. APP 已经进入后台

小结

NSCache + NSMapTable 双重缓存机制可保证 SDWebImage 内部缓存在收到内存警告而释放资源后,还能更快速的从当前 APP 的其他地方获取到这张图片。customPaths 传入的目录数组仅用于读操作,默认所有的 io 操作都在 ioQueue 中串行执行。

Downloader

SDWebImageDownloader

图片下载管理器,管理每个图片下载操作,并控制其生命周期。

  1. 所有的图片下载操作都放在 NSOperationQueue 并发操作队列 downloadQueue 中,最大并发数为6
  2. 每个 URL 所对应的下载操作都放在 URLOperations 中,当 URLOperations 不存在该 URL 所对应的下载操作 id 时,才创建新的下载操作 SDWebImageDownloaderOperation 对象,并存入 URLOperations 中,如果已存在 SDWebImageDownloaderOperation 对象operation,则将 progressBlockcompletedBlock 保存到 SDWebImageDownloaderOperation 对象的 callbackBlocks 回调数组中
  3. 作为 NSURLSessionNSURLSessionDataTask 代理
  4. 下载操作队列默认采用 FIFO 先进先出,可设置为 LIFO 后进先出
  5. 返回 SDWebImageDownloadToken 对象作为下载操作对象,多次调用 URL 的下载,返回的 SDWebImageDownloadToken 不同,但其属性 downloadOperation 却是同一个下载操作对象

SDWebImageDownloaderOperation

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    LOCK(self.callbacksLock);
    [self.callbackBlocks addObject:callbacks];
    UNLOCK(self.callbacksLock);
    return callbacks;
}
- (BOOL)cancel:(nullable id)token {
    BOOL shouldCancel = NO;
    LOCK(self.callbacksLock);
    [self.callbackBlocks removeObjectIdenticalTo:token];
    if (self.callbackBlocks.count == 0) {
        shouldCancel = YES;
    }
    UNLOCK(self.callbacksLock);
    if (shouldCancel) {
        [self cancel];
    }
    return shouldCancel;
}

处理 URL 对应的具体下载操作,自主管理下载操作状态。callbackBlocks 可变数组存储每一个回 SDWebImageDownloaderProgressBlockSDWebImageDownloaderCompletedBlock 回调。

取消下载操作的时候,只是将想要取消的操作所对应的 token (即 SDWebImageDownloaderProgressBlockSDWebImageDownloaderCompletedBlock 的键值对) 从 callbackBlocks 数组中移除。

当可变数组 callbackBlocks 中的回调数为0的时候,才会取消本次下载操作。

设置 optionSDWebImageDownloaderProgressiveDownload 可边下载边回调,正常则在图片下载完成后,在callCompletionBlocksWithImage:imageData:error:finished:中回调图片数据:

- (void)callCompletionBlocksWithImage:(nullable UIImage *)image
                            imageData:(nullable NSData *)imageData
                                error:(nullable NSError *)error
                             finished:(BOOL)finished {
    NSArray *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
    dispatch_main_async_safe(^{
        for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
            completedBlock(image, imageData, error, finished);
        }
    });
}

如果存在多个回调,则按照添加的顺序回调的。

小结

对同一个 URL 的多次下载操作,只会生成一个 operation 对象,只有当 URL 无对应回调时,才会真正取消该下载操作

主体 Utils

SDWebImageCombinedOperation

内存缓存查询操作和图片下载操作的结合体,即包含了 SDWebImage 框架获取一张图片的2个主要耗时操作。

  • NSOperation *cacheOperation 耗时的磁盘查询操作,
  • SDWebImageDownloadToken *downloadToken 网络图片下载操作

SDWebImageManager

在我们的平时使用中,很少直接操作SDWebImageDownloaderSDImageCache去下载保存图片,大都是通过SDWebImageManager来管理,即使通过UIImageView+WebCache等分类加载图片,最后也会使用SDWebImageManager来处理。

加载图片的方法为 loadImageWithURL:options:progress:completed:

  1. 判断 URL 的长度是否大于0;URL 是否在 failedURLs 集合中(存放网络资源异常的 URL 集合),若在,options 是否包含 SDWebImageRetryFailed

  2. 创建一个 SDWebImageCombinedOperation 对象,保存在 runningOperations 集合中;

  3. 从内存缓存 SDImageCache 中查询图片,并将返回的 NSOperation 赋值给 SDWebImageCombinedOperation 对象的 cacheOperation 属性

  4. 当没缓存图片或要求刷新数据的时候,通过 SDWebImageDownloader 下载图片,并将返回的 SDWebImageDownloadToken 对象赋值给 SDWebImageCombinedOperation 对象的 downloadToken 属性

SDWebImagePrefetcher

批预下载管理器,提前下载一批 URLs 所对应的图片,每次只能处理一批图片组。使用的图片管理器并不是 SDWebImageManager 单例,而且单独创建的实例对象;可设置最大并发数,默认为3。

主要用于提前下载图片数据,不依赖于 UI 层。

SDWebImageTransition

设置图片的过渡效果

Decoder

图片解码,讲图片二进制数据 NSData 解码出 UIImage,或将 UIImage 编码成 NSData

SDWebImageCodersManager

图片编解码管理器,可通过 addCoder:removeCoder: 添加或移除解码器,coders 可变数组用于存放当前的所有解码器。默认只有 SDWebImageImageIOCoder 解码器

- (BOOL)canEncodeToFormat:(SDImageFormat)format {
    LOCK(self.codersLock);
    NSArray> *coders = self.coders;
    UNLOCK(self.codersLock);
    for (id coder in coders.reverseObjectEnumerator) {
        if ([coder canEncodeToFormat:format]) {
            return YES;
        }
    }
    return NO;
}

通过 SDWebImageCodersManager 编解码图片的时候,根据图片的二进制数据的第一个字节,获取图片格式类型,逆遍历 coders 中的所有解码器,直到遇到可以成功解码该格式的解码器为止。

SDWebImageImageIOCoder

支持 PNG, JPEG, TIFF 格式,同时也支持 GIF 格式,但是只会解码出第一帧的图片

SDWebImageGIFCoder

GIF 格式的专用解码器,通过 CGImage 遍历解码出 GIF 动图

SDWebImageWebPCoder

WebP 格式的专用解码器,若想解码出 WebP 格式的图片,需要单独导入 WebP 相关的库 pod 'SDWebImage/WebP'

小结

SDWebImage 4.0.0 之前,是可以直接设置 GIF 动图的,但是在 4.0.0 之后,加载的 GIF 动图只显示第一帧的图像。有2种方式显示网络上的 GIF 动图:

  • 调用 SDWebImageCodersManageraddCoder:方法注册 SDWebImageGIFCoder 解码器
  • 再单独导入 FLAnimatedImagepod 'SDWebImage/GIF',用 FLAnimatedImageView 替换 UIImageView

第二种方法的性能比第一种高

WebCache Categories

UIImageViewUIButtonNSButtonMKAnnotationView 等常用图片容易扩充异步图片加载方法。

UIImageView+WebCache 采用 UIView+WebCache 默认的赋值方式(统一当成 UIImageView 处理);而 UIButton+WebCache 则自己实现了 setImageBlockMKAnnotationView+WebCache 也是自己实现了 setImageBlock

UIView+WebCache

UIImageViewUIButtonMKAnnotationView 三个类的分类最后也是调用到了 UIView+WebCachesd_internalSetImageWithURL:placeholderImage:options:operationKey:internalSetImageBlock:progress:completed:context: 方法上:

  1. 根据 operationKey (默认为对象类名)先将上次的加载图片操作 id operation 取消
  2. 设置占位图片
  3. 通过 SDWebImageManager 图片管理器加载图片
  4. SDWebImageManager 返回的id operation 和当前操作符 operationKey 绑定保存在 sd_operationDictionary 可变哈希映射表中

UIImageView+WebCache

- (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];
}

对应的操作符 operationKey 为类名,而setImageBlock 也是 UIView+WebCache 默认以 UIImageView 处理

iOS 应用和 TV 应用

- (void)sd_setAnimationImagesWithURLs:(nonnull NSArray *)arrayOfURLs {
    [self sd_cancelCurrentAnimationImagesLoad];
    NSPointerArray *operationsArray = [self sd_animationOperationArray];
    
    [arrayOfURLs enumerateObjectsUsingBlock:^(NSURL *logoImageURL, NSUInteger idx, BOOL * _Nonnull stop) {
        __weak __typeof(self) wself = self;
        id  operation = [[SDWebImageManager sharedManager] loadImageWithURL:logoImageURL options:0 progress:nil completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong typeof(wself) sself = wself;
            if (!sself) return;
            dispatch_main_async_safe(^{
                [sself stopAnimating];
                if (sself && image) {
                    NSMutableArray *currentImages = [[sself animationImages] mutableCopy];
                    if (!currentImages) {
                        currentImages = [[NSMutableArray alloc] init];
                    }
                    
                    // We know what index objects should be at when they are returned so
                    // we will put the object at the index, filling any empty indexes
                    // with the image that was returned too "early". These images will
                    // be overwritten. (does not require additional sorting datastructure)
                    while ([currentImages count] < idx) {
                        [currentImages addObject:image];
                    }
                    
                    currentImages[idx] = image;

                    sself.animationImages = currentImages;
                    [sself setNeedsLayout];
                }
                [sself startAnimating];
            });
        }];
        @synchronized (self) {
            [operationsArray addPointer:(__bridge void *)(operation)];
        }
    }];
}

static char animationLoadOperationKey;

// element is weak because operation instance is retained by SDWebImageManager's runningOperations property
// we should use lock to keep thread-safe because these method may not be acessed from main queue
- (NSPointerArray *)sd_animationOperationArray {
    @synchronized(self) {
        NSPointerArray *operationsArray = objc_getAssociatedObject(self, &animationLoadOperationKey);
        if (operationsArray) {
            return operationsArray;
        }
        operationsArray = [NSPointerArray weakObjectsPointerArray];
        objc_setAssociatedObject(self, &animationLoadOperationKey, operationsArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        return operationsArray;
    }
}

- (void)sd_cancelCurrentAnimationImagesLoad {
    NSPointerArray *operationsArray = [self sd_animationOperationArray];
    if (operationsArray) {
        @synchronized (self) {
            for (id operation in operationsArray) {
                if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                    [operation cancel];
                }
            }
            operationsArray.count = 0;
        }
    }
}

对于这两个平台的应用,对 UIImageView 额外新增图片组的异步加载方法

UIImageView+HighlightedWebCache

- (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url
                              options:(SDWebImageOptions)options
                             progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDExternalCompletionBlock)completedBlock {
    __weak typeof(self)weakSelf = self;
    [self sd_internalSetImageWithURL:url
                    placeholderImage:nil
                             options:options
                        operationKey:@"UIImageViewImageOperationHighlighted"
                       setImageBlock:^(UIImage *image, NSData *imageData) {
                           weakSelf.highlightedImage = image;
                       }
                            progress:progressBlock
                           completed:completedBlock];
}

对应的操作符 operationKeyUIImageViewImageOperationHighlighted,而setImageBlock 则是自定义

UIButton+WebCache

- (void)sd_setImageWithURL:(nullable NSURL *)url
                  forState:(UIControlState)state
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                 completed:(nullable SDExternalCompletionBlock)completedBlock {
    if (!url) {
        [self.sd_imageURLStorage removeObjectForKey:imageURLKeyForState(state)];
    } else {
        self.sd_imageURLStorage[imageURLKeyForState(state)] = url;
    }
    
    __weak typeof(self)weakSelf = self;
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:imageOperationKeyForState(state)
                       setImageBlock:^(UIImage *image, NSData *imageData) {
                           [weakSelf setImage:image forState:state];
                       }
                            progress:nil
                           completed:completedBlock];
}
- (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url
                            forState:(UIControlState)state
                    placeholderImage:(nullable UIImage *)placeholder
                             options:(SDWebImageOptions)options
                           completed:(nullable SDExternalCompletionBlock)completedBlock {
    if (!url) {
        [self.sd_imageURLStorage removeObjectForKey:backgroundImageURLKeyForState(state)];
    } else {
        self.sd_imageURLStorage[backgroundImageURLKeyForState(state)] = url;
    }
    
    __weak typeof(self)weakSelf = self;
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:backgroundImageOperationKeyForState(state)
                       setImageBlock:^(UIImage *image, NSData *imageData) {
                           [weakSelf setBackgroundImage:image forState:state];
                       }
                            progress:nil
                           completed:completedBlock];
}

UIButton 的 image 和 backgroundImage 所对应的 operationKey 根据不同的状态 state 而不同,setImageBlock 也不一样

Other

MKAnnotationView 的做法和 UIImageView 基本一致的,而 NSButton 则是和 UIButton 基本一致。

End

本文是对 SDWebImage 简单用法所涉及到的类进行一些简单的源码解析。这次的分析是基于 4.3.0 的解析

你可能感兴趣的:(SDWebImage简单解析)