SDWebImage源码解析

作者: vicentwyh | 来源:发表于2020-09-17 00:05 被阅读0次

    SDWebImage是一个开源的第三方库,支持从远程服务器下载并缓存图片的功能。它具有以下功能:

    • 提供UIImageView的一个分类,以支持网络图片的加载与缓存管理
    • 一个异步的图片下载器
    • 一个图片内存缓存+异步的磁盘缓存
    • 支持GIF图片
    • 支持WebP图片
    • 后台图片解压缩处理
    • 确保同一个URL的图片不被下载多次
    • 确保虚假的URL不会被反复加载
    • 确保下载及缓存时,主线程不被阻塞

    UIImageView+WebCache.h

    1. SDWebImageCompat.h: 定义一些宏定义
    2. SDWebImageDefine.h:
      定义关于图片操作的option(SDWebImageOptions ):图片读取操作、图片下载优先级、图片后台下载、图片下载完成展示操作等
      key:
    3. UImageView+WebCache.h
      最上层对外接口,对ImageVIew的扩展,提供图片设置、占位图设置、图片操作、下载过程回调、下载完成回调操作
    
    /// 异步下载和异步缓存图片
    /// @param url url
    /// @param placeholder 展位图
    /// @param options 图片操作,具体参考 SDWebImageOptions.h
    /// @param context <#context description#>
    /// @param progressBlock 图片下载回调,包括已下载大小、全部大小、url
    /// @param completedBlock 图片下载回调,包括图片、下载失败错误信息、图片获取类型(SDImageCacheType:内存、硬盘、远程下载)
    - (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.h内部的方法

    - (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;
    
    1. UIView+WebCache.h内部实现大致为以下省略版:
      最终会调用SDWebImageManager-loadImageWithURL: options:progress:completed
    - (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 {se'
        
        //删除该对象之前的下载操作
        [self sd_cancelImageLoadOperationWithKey:NSStringFromClass(self.class)];
        self.sd_imageURL = url;
        
        //展示占位图 placeholder
        if (!(options & SDWebImageDelayPlaceholder)) {
            dispatch_main_async_safe(^{
                [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
            });
        }
        
        SDWebImageManager *manager = [SDWebImageManager sharedManager];
        if (url) {
            //url不为nil,进一步获取图片处理,并返回一个NSOperation的对象
            @weakify(self);
    
            id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
                
                //url处理完成回调
                SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                    if (completedBlock) {
                        completedBlock(image, data, error, cacheType, finished, url);
                    }
                };
                
                //展示url下载的图片
                UIImage *targetImage = image;
                NSData *targetData = data;
                dispatch_main_async_safe(^{
                    [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
    
                    callCompletedBlockClojure();
                });
            }];
            [self sd_setImageLoadOperation:operation forKey:validOperationKey];
        } else {
            //url为nil回调
            dispatch_main_async_safe(^{
                if (completedBlock) {
                    NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
                    completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
                }
            });
        }
    }
    
    

    类似的,SDWebImage也提供了 UIButton+WebCacheMKAnnotationView+WebCache,方便使用

    SDWebImageManager.h

    SDWebImageManager处理图片url,获取图片后回调,其内部获取图片的过程大致为两个步骤:

    • 首先会交由 SDImageCache处理, 从缓存中(内存+本地硬盘)查找图片
    • 如果缓存中不存在图片,再交由SDWebImageDownloader下载图片
    • 图片下载完成,缓存到到内存和本地磁盘中

    获取图片入口

    - (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                              options:(SDWebImageOptions)options
                                              context:(nullable SDWebImageContext *)context
                                             progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                            completed:(nonnull SDInternalCompletionBlock)completedBlock {
        ...
        SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
        operation.manager = self;
    
        //信号量,开启线程保护,防止多线程操作添加多个operation
        dispatch_semaphore_wait(self.runningOperationsLock, DISPATCH_TIME_FOREVER);
        [self.runningOperations addObject:operation];
        dispatch_semaphore_signal(self.runningOperationsLock)
    
        // 从缓存中查找 url 对应的图片
        [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
        return operation;
    }
    
    //缓存中查找图片
    - (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                     url:(nonnull NSURL *)url
                                 options:(SDWebImageOptions)options
                                 context:(nullable SDWebImageContext *)context
                                progress:(nullable SDImageLoaderProgressBlock)progressBlock
                               completed:(nullable SDInternalCompletionBlock)completedBlock {
       
        id<SDImageCache> imageCache = [SDImageCache sharedImageCache];;
        
        //url作为图片的标识符key
        NSString *key = [self cacheKeyForURL:url context:context];
    
        //在缓存中查找图片是否下载过
        @weakify(operation);
        operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
            @strongify(operation);
            if (!operation || operation.isCancelled) {
                // 图片下载操作被取消
                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
                [self safelyRemoveOperationFromRunning:operation];
                return;
            }
            //图片未下载完毕,继续下载
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
    
         // 缓存中不存在,开始下载图片
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
    }
    
    

    本地不存在图片,开始下载

    - (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                        url:(nonnull NSURL *)url
                                    options:(SDWebImageOptions)options
                                    context:(SDWebImageContext *)context
                                cachedImage:(nullable UIImage *)cachedImage
                                 cachedData:(nullable NSData *)cachedData
                                  cacheType:(SDImageCacheType)cacheType
                                   progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                  completed:(nullable SDInternalCompletionBlock)completedBlock {
        
        id<SDImageLoader> imageLoader = self.imageLoader = [SDWebImageDownloader sharedDownloader];;
        
        // 当本地没有缓存的图片,或者需要刷新缓存时,下载新图片
        BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly)
        & (!cachedImage || options & SDWebImageRefreshCached)
        & [imageLoader canRequestImageForURL:url];
        
        if (shouldDownload) {
            if (cachedImage && options & SDWebImageRefreshCached) {
                [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
                // Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.
                SDWebImageMutableContext *mutableContext [context mutableCopy];
                mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
                context = [mutableContext copy];
            }
            
            @weakify(operation);
            // 1、下载图片
            operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                @strongify(operation);
                // 图片下载过程异常的回调
                if (!operation || operation.isCancelled) { //用户取消
                    ...
                } else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
                    ...
                } else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {
                    ...
                } else if (error) {
                    [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
                } else {
                    // 2、存储图片到缓存
                    [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
                }
                 //从集合中移除下载操作
                if (finished) {
                    [self safelyRemoveOperationFromRunning:operation];
                }
            }];
        } else if (cachedImage) {
            [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        } else {
            // Image not in cache and download disallowed by delegate
            [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }
    }
    
    

    存储图片到缓存中

    - (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                          url:(nonnull NSURL *)url
                                      options:(SDWebImageOptions)options
                                      context:(SDWebImageContext *)context
                              downloadedImage:(nullable UIImage *)downloadedImage
                               downloadedData:(nullable NSData *)downloadedData
                                     finished:(BOOL)finished
                                     progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                    completed:(nullable SDInternalCompletionBlock)completedBlock {
        // 图片缓存key
        SDWebImageMutableContext *originContext = [context mutableCopy];
        NSString *key = [self cacheKeyForURL:url context:originContext];
        
        // 存储图片
        [self storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:SDImageCacheTypeAll options:options context:context completion:^{
            ...
        }];
    }
    
    - (void)storeImage:(nullable UIImage *)image
             imageData:(nullable NSData *)data
                forKey:(nullable NSString *)key
             cacheType:(SDImageCacheType)cacheType
               options:(SDWebImageOptions)options
               context:(nullable SDWebImageContext *)context
            completion:(nullable SDWebImageNoParamsBlock)completion {
    
        id<SDImageCache> imageCache = [SDImageCache sharedImageCache];
        [imageCache storeImage:image imageData:data forKey:key cacheType:cacheType completion:^{
            if (completion) {
                completion();
            }
        }];
    }
    

    "SDWebImageError.h包含定义了错误码枚举的文件:

    typedef NS_ERROR_ENUM(SDWebImageErrorDomain, SDWebImageError) {}
    

    图片缓存读取、存储

    SDImageCache + SDMemoryCache + SDDiskCache

    SDMemoryCache初始化:

    • 创建串行队列dispatch_queue_t,用于异步存储、读取本地磁盘图片,避免阻塞主线程,同时保证图片资源访问的线程安全
    • SDMemoryCache(继承自NSCache),缓存内存图片,方便下次快速访问相同图片
    • SDDiskCache缓存本地磁盘图片
    • 注册消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。

    图片读取、存储:

    默认情况下包括两个过程,可根据需求进行配置:

    • 内存图片读取、存储
    • 本地磁盘图片读取、存储,异步执行
    //存储图片
    - (void)storeImage:(nullable UIImage *)image
             imageData:(nullable NSData *)imageData
                forKey:(nullable NSString *)key
              toMemory:(BOOL)toMemory
                toDisk:(BOOL)toDisk
            completion:(nullable SDWebImageNoParamsBlock)completionBlock {
        // 1、存储到内存中
        if (toMemory) {
            NSUInteger cost = image.sd_memoryCost;
            [self.memoryCache setObject:image forKey:key cost:cost];
        }
        
        // 2、异步存储到本地磁盘
        if (toDisk) {
            dispatch_async(self.ioQueue, ^{
                @autoreleasepool {
                    [self _storeImageDataToDisk:imageData forKey:key];
                }
                
                if (completionBlock) {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        completionBlock();
                    });
                }
            });
        } else {
            if (completionBlock) {
                completionBlock();
            }
        }
    }
    
    //读取图片
    - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
        
        // 1、首先检查内存中是否存在相应图片
        UIImage *image;
        if (queryCacheType != SDImageCacheTypeDisk) {
            image = [self imageFromMemoryCacheForKey:key];
        }
    
        BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData));
        if (shouldQueryMemoryOnly) {
            if (doneBlock) {
                doneBlock(image, nil, SDImageCacheTypeMemory);
            }
            return nil;
        }
        
        // 2、检查磁盘中会否存在图片
        NSOperation *operation = [NSOperation new];
        BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
                                    (!image && options & SDImageCacheQueryDiskDataSync));
        void(^queryDiskBlock)(void) =  ^{
    
            @autoreleasepool {
                //从磁盘中查找图片数据
                NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
                UIImage *diskImage;
                SDImageCacheType cacheType = SDImageCacheTypeNone;
                
                /*
                 内存中存在图片,则取内存图片;
                 内存中不存在,则取磁盘图片,同时添加到内存中,方便下次快速访问此图片
                 */
                if (image) {
                    diskImage = image;
                    cacheType = SDImageCacheTypeMemory;
                } else if (diskData) {
                    cacheType = SDImageCacheTypeDisk;
                    diskImage = [self diskImageForKey:key data:diskData options:options context:context];
                    if (diskImage) {
                        //图片保存在内存中
                        NSUInteger cost = diskImage.sd_memoryCost;
                        [self.memoryCache setObject:diskImage forKey:key cost:cost];
                    }
                }
                
                if (doneBlock) {
                    if (shouldQueryDiskSync) {
                        doneBlock(diskImage, diskData, cacheType);
                    } else {
                        dispatch_async(dispatch_get_main_queue(), ^{
                            doneBlock(diskImage, diskData, cacheType);
                        });
                    }
                }
            }
        };
        
        // 在ioQueue中进行查询以保持IO安全
        if (shouldQueryDiskSync) {
            dispatch_sync(self.ioQueue, queryDiskBlock);
        } else {
            dispatch_async(self.ioQueue, queryDiskBlock);
        }
        
        return operation;
    }
    
    内存图片读取、存储:

    由于NSCahe类的缓存释放时间完全由系统管理,我们无法得知NSCahe缓存的释放时间,这样可能在我们需要这个缓存的时候,系统却已经把这个缓存给释放了。因此,子类SDMemoryCache初始化时维护了一个引用表 NSMapTable,强引用key,弱引用value,用来在需要某个缓存的时候,确定这个缓存不会被释放。同时使用信号量保证多线程安全访问引用表NSMapTable

    self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
    
    self.weakCacheLock = dispatch_semaphore_create(1);
    

    具体实现过程

    1. SDMemoryCache重写了NSCache的核心方法- setObject: forKey: cost:,存储到系统管理的NSCache内存时,同时存储一份引用到引用表NSMapTable
    - (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
            SD_LOCK(self.weakCacheLock);
            [self.weakCache setObject:obj forKey:key];
            SD_UNLOCK(self.weakCacheLock);
        }
    }
    
    1. 重载了NSCache的- objectForKey:
      1)首先在系统的NSCache里查找,因为NSCache的释放完全由系统管理,所以取值的时候很可能value已经被系统释放了。
      2)如果NSCache内存里没有找到,或者已经被系统释放了,在弱引用表NSMapTable查找,如果有值,则添加到NSCache内存中,这样在下次再取值的时候,如果NSCache中的value没有被释放,我们就能直接拿来用

    这样保证了每次取value的时候,就算NSCache的那一份已经释放了,我们自己存的还能拿出来用。用内存空间换取了查询时间。尽可能提高效查询效率的同时,又保证了访问过的数据不被释放

    - (id)objectForKey:(id)key {
        id obj = [super objectForKey:key];
        if (key && !obj) {
            // Check weak cache
            SD_LOCK(self.weakCacheLock);
            obj = [self.weakCache objectForKey:key];
            SD_UNLOCK(self.weakCacheLock);
            if (obj) {
                // Sync cache
                NSUInteger cost = 0;
                if ([obj isKindOfClass:[UIImage class]]) {
                    cost = [(UIImage *)obj sd_memoryCost];
                }
                [super setObject:obj forKey:key cost:cost];
            }
        }
        return obj;
    }
    

    本地磁盘图片读取、存储:

    图片存储的磁盘位置diskCachePath:沙盒->Library->Cache

    NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject
    

    图片存储的文件名称url md5加密后的字符串

    多线程安全:SDImageCache使用串行队列调用SDDiskCache存储、读取磁盘图片

    使用NSFileManager文件管理器创建相应文件目录,将图片数据写入目录下

    - (void)setData:(NSData *)data forKey:(NSString *)key {
        if (![self.fileManager fileExistsAtPath:self.diskCachePath]) {
            [self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
        }
        
        // 对url进行md5加密,作为图片路径名称
        NSString *cachePathForKey = [self cachePathForKey:key];
        
        //存储到本地磁盘
        NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
        [data writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
        
        // 不要被备份到iClound
        if (self.config.shouldDisableiCloud) {
            [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
        }
    }
    
    - (NSData *)dataForKey:(NSString *)key {
        NSParameterAssert(key);
        NSString *filePath = [self cachePathForKey:key];
        NSData *data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
        if (data) {
            return data;
        }
        data = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil];
        if (data) {
            return data;
        }
        
        return nil;
    }
    

    注意:存储在本地的图片格式为PNG、JPG等,这是经过编码压缩后的图片数据,不是位图,要把它们渲染到屏幕前就需要进行解码转成位图数据(因为位图体积很大,所以磁盘缓存不会直接缓存位图数据,而是编码压缩后的PNG或JPG数据),而这个解码操作比较耗时,iOS默认是在主线程解码,所以SDWebImage将这个过程放到子线程了。具体的过程下面会说明。

    清除缓存图片:

    • 内存图片清除:SDMemoryCache继承自NSCache,自动响应内存警告,实现缓存图片清除
    • 本地磁盘图片清除:SDImageCache注册了通知监听,当应用结束运行时,使用UIApplication的如下方法,使用应用继续运行,调用SDDiskCache删除过期文件方法清除磁盘缓存图片
    - (UIBackgroundTaskIdentifier)beginBackgroundTaskWithExpirationHandler:(void(^ __nullable)(void))handler;
    

    调用SDDiskMemory删除过期文件:

    1. 删除过期文件
    2. 如果磁盘图片缓存依然大于限制的大小,继续清除最旧的图片
    - (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
        dispatch_async(self.ioQueue, ^{
            [self.diskCache removeExpiredData];
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    }
    
    - (void)removeExpiredData {
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        
        // Compute content date key to be used for tests
        NSURLResourceKey cacheContentDateKey = NSURLContentModificationDateKey;
        switch (self.config.diskCacheExpireType) {
            case SDImageCacheConfigExpireTypeAccessDate:
                cacheContentDateKey = NSURLContentAccessDateKey;
                break;
            case SDImageCacheConfigExpireTypeModificationDate:
                cacheContentDateKey = NSURLContentModificationDateKey;
                break;
            case SDImageCacheConfigExpireTypeCreationDate:
                cacheContentDateKey = NSURLCreationDateKey;
                break;
            case SDImageCacheConfigExpireTypeChangeDate:
                cacheContentDateKey = NSURLAttributeModificationDateKey;
                break;
            default:
                break;
        }
        
        NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey];
        
        // This enumerator prefetches useful properties for our cache files.
        NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];
        
        NSDate *expirationDate = (self.config.maxDiskAge < 0) ? nil: [NSDate dateWithTimeIntervalSinceNow:-self.config.maxDiskAge];
        NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;
        
         /*
            枚举缓存目录中的所有文件。 这个循环有两个目的:
            1.删除过期日期之前的文件。
            2.存储基于大小的清理过程的文件属性。
        */
        NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSError *error;
            NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
            
            // Skip directories and errors.
            if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }
            
            // 删除过期文件
            NSDate *modifiedDate = resourceValues[cacheContentDateKey];
            if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }
            
            // Store a reference to this file and account for its total size.
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
            cacheFiles[fileURL] = resourceValues;
        }
        
        for (NSURL *fileURL in urlsToDelete) {
            [self.fileManager removeItemAtURL:fileURL error:nil];
        }
        
        // 如果剩余的磁盘缓存超过配置的最大大小,请执行基于大小的清理操作 我们先删除最旧的文件
        NSUInteger maxDiskSize = self.config.maxDiskSize;
        if (maxDiskSize > 0 && currentCacheSize > maxDiskSize) {
            // Target half of our maximum cache size for this cleanup pass.
            const NSUInteger desiredCacheSize = maxDiskSize / 2;
            
            // 以最新修改时间重新排序剩余文件
            NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                     usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                         return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
                                                                     }];
            
            // Delete files until we fall below our desired cache size.
            for (NSURL *fileURL in sortedFiles) {
                if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
                    
                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
    }
    
    

    图片下载

    SDWebImageDownloader + SDWebImageDownloaderOperation

    SDWebImageDownloader:

    实现异步图片下载,SDWebImageDownloader初始化包括:

    • 下载配置SDWebImageDownloaderConfig下载超时时间15s、最大并发数6、FIFO的图片下载顺序
    • 线程下载队列NSOperationQueue防止阻塞主线程
    • NSMutableDictionary哈希表管理下载操作NSOperation,以url为key,避免同一个URL的图片下载多次
    • 下载会话管理NSURLSession

    图片下载最终调用- downloadImageWithURL:options:context:progress:completed:

    1. 信号量dispatch_semaphore_t开启线程保护
    2. 以url为key在查询哈希表NSMutableDictionary,查找对应的下载操作NSOperation
    3. 哈希表中不存在相应的NSOperation,就创建一个 SDWebImageDownloaderOperation(继承自NSOperation)的下载操作
    4. 缓存 operation 到哈希表中,下次查询使用,避免同一个url下载多次
    5. operation添加到 NSOperationQueue 开始下载

    代码大致如下:

    - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                       options:(SDWebImageDownloaderOptions)options
                                                       context:(nullable SDWebImageContext *)context
                                                      progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                     completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
        //1. 信号量多线程保护
        SD_LOCK(self.operationsLock);
        id downloadOperationCancelToken;
        
        //2.哈希表中以url为key获取缓存的下载操作NSOperation
        NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
        
        //哈希表不存在相应下载操作Operation,或者操作已被取消,重新创建一个SDWebImageDownloaderOperation(继承NSOperation),添加到队列中开始下载,并缓存到哈希表中方便查询
        if (!operation || operation.isFinished || operation.isCancelled) {
            //3.创建一个Operation
            operation = [self createDownloaderOperationWithUrl:url options:options context:context];
    
            //Operation执行完毕后从哈希表URLOperations中移除
            @weakify(self);
            operation.completionBlock = ^{
                @strongify(self);
                SD_LOCK(self.operationsLock);
                [self.URLOperations removeObjectForKey:url];
                SD_UNLOCK(self.operationsLock);
            };
            //4.缓存到哈希表中
            self.URLOperations[url] = operation;
            
           // 该方法将 progressBlock 和 completedBlock 缓存到 operation 内部定义的哈希表,以便在下载的时候执行相应的回调操作
            downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
            
            //5.添加下载操作operation到队列NSOperationQueue中,开始下载
            [self.downloadQueue addOperation:operation];
        }
        SD_UNLOCK(self.operationsLock);
        
        //返回一个包含operatin的管理实例,用于SDWebImageManager取消下载时使用
        SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
        token.url = url;
        token.request = operation.request;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
        
        return token;
    }
    

    其中创建SDWebImageDownloaderOperation的方法createDownloaderOperationWithUrl:options:context:大体为:

    1. 创建NSURLRequest,配置请求头allHTTPHeaderFields、缓存策略cachePolicy、cookis、请求管线等
    2. 创建SDWebImageDownloaderOperation
    - (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url
                                                                                      options:(SDWebImageDownloaderOptions)options
                                                                                      context:(nullable SDWebImageContext *)context {
    
        // 1.创建NSURLRequest,进行配置
        NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
        //配置NSURLRequest
        ...
    
        // 2.创建SDWebImageDownloaderOperation
        NSOperation<SDWebImageDownloaderOperation> *operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request inSession:self.session options:options context:context];
        //配置SDWebImageDownloaderOperation
        ...
    
        return operation;
    }
    

    上面创建的SDWebImageDownloadOperation(继承自NSOperation)添加到线程队列NSOperationQueue后开始执行对应的系统方法-start,SDWebImageDownloadOperation重载了-start实现:

    SDWebImageDownloadOperation:

    初始化包括:

    • 线程下载队列NSOperationQueue,最大并发数1,用于异步解码图片数据
    • Bool值存储执行状态、图片下载完成状态
      等等

    创建 NSURLSessionDataTask,开始执行图片下载

    - (void)start {
        
        // 1、线程保护,创建NSURLSessionDataTask
        @synchronized (self) {
            if (self.isCancelled) {
                self.finished = YES;
                [self reset];
                return;
            }
    
            NSURLSession *session = self.unownedSession;
            self.dataTask = [session dataTaskWithRequest:self.request];
            self.executing = YES;
        }
    
        // 2、开始执行下载任务
        [self.dataTask resume];
        
        // 3、执行对应的 progressBlock 回调操作
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
    }
    

    图片下载过程调用NSURLSession的代理方法,一般情况下代理对象为SDWebImageDownloader,进而在持有的线程队列downloadQueue(NSOperationQueue)中查找相应NSURLDataTask的SDWebImageDownloaderOperation,交由其处理。如果代理对象SDWebImageDownloaderOperation,直接调用相应代理实现:

    以下载完成的代理执行为例,过程如下,可忽略:

    - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
        
        NSOperation<SDWebImageDownloaderOperation> *dataOperation = nil;
        for (NSOperation<SDWebImageDownloaderOperation> *operation in self.downloadQueue.operations) {
            if ([operation respondsToSelector:@selector(dataTask)]) {
                // 线程安全保护
                NSURLSessionTask *operationTask;
                @synchronized (operation) {
                    operationTask = operation.dataTask;
                }
                if (operationTask.taskIdentifier == task.taskIdentifier) {
                    dataOperation = operation;
                    break;
                }
            }
        }
        if ([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
            [dataOperation URLSession:session task:task didCompleteWithError:error];
        }
    }
    

    图片下载完成后,在线程队列中异步解码图片

    - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
        
        // 发送通知图片下载完成
        ...
        
        // 图片解码
        if (!error) {
            NSData *imageData = [self.imageData copy];
            if (imageData) {
               //  异步解码图片数据
                [self.coderQueue cancelAllOperations];
                [self.coderQueue addOperationWithBlock:^{
                    //图片解码
                    UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
    
                    //回调操作block,结束任务、清除sessiond索引、清除回调block索引
                    [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
                    [self done];
                }];
            }
        }
    }
    

    图片解码

    入口:SDImageLoader.h、SDImageCacheDefine.h
    提供了图片二进制数据解码为图片的方法

    SDImageLoader:

    UIImage SDImageLoaderDecodeImageData(NSData *imageData, NSURL * imageURL, SDWebImageOptions options, SDWebImageContext * context) 
    

    SDImageCacheDefine:

    UIImage * SDImageCacheDecodeImageData(NSData * imageData, NSString * cacheKey, SDWebImageOptions options, SDWebImageContext * context);
    

    两者的实现基本上一致,区别只是SDImageLoader需要将imageURL转化为图片对应的标识cachekey。
    它们都只是提供了图片解码的入口,实现过程分为两步:
    1.二进制数据解码为图片:具体的实现交由 SDImageCodersManager管理的解码器实现

    1. 图片解码为位图:由 SDImageCoderHelper实现
    UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
        NSCParameterAssert(imageData);
        NSCParameterAssert(imageURL);
        
        UIImage *image;
        NSString *cacheKey = imageURL.absoluteString;
        BOOL decodeFirstFrame = NO;
        CGFloat scale = 1;
        BOOL shouldScaleDown = NO;
        
        SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2];
        mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
        mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
        mutableCoderOptions[SDImageCoderWebImageContext] = context;
        SDImageCoderOptions *coderOptions = [mutableCoderOptions copy];
        
        // Grab the image coder
        id<SDImageCoder> imageCoder = [SDImageCodersManager sharedManager];
        
        if (!decodeFirstFrame) {
            // check whether we should use `SDAnimatedImage`
            Class animatedImageClass = context[SDWebImageContextAnimatedImageClass];
            if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)]) {
                image = [[animatedImageClass alloc] initWithData:imageData scale:scale options:coderOptions];
                if (image) {
                    // Preload frames if supported
                    if (options & SDWebImagePreloadAllFrames && [image respondsToSelector:@selector(preloadAllFrames)]) {
                        [((id<SDAnimatedImage>)image) preloadAllFrames];
                    }
                } else {
                    // Check image class matching
                    if (options & SDWebImageMatchAnimatedImageClass) {
                        return nil;
                    }
                }
            }
        }
        
        // 交由 SDImageCodersManager 进行图片解码
        if (!image) {
            image = [imageCoder decodedImageWithData:imageData options:coderOptions];
        }
        
        //是否需要解码为位图
        if (image) {
            
            BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
            if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
                shouldDecode = NO;
            } else if (image.sd_isAnimated) {
                shouldDecode = NO;
            }
            if (shouldDecode) {
                //转化为位图
                image = [SDImageCoderHelper decodedImageWithImage:image];
            }
        }
        
        return image;
    }
    

    管理:SDImageCodersManager
    目前该管理默认提供三种解码器进行二进制数据解码,也可以自定义解码器添加到该类中:

    • SDImageIOCoder:其他图片类型
    • SDImageGIFCoder(继承自SDImageIOAnimatedCoder):解码gif图片
    • SDImageAPNGCoder(继承自SDImageIOAnimatedCoder):解码png图片

    当调用SDImageCodersManager进行解码时,根据图片类型,选用对应的解码器

    图片类型

    通过图片数据的第一字节数据大小以及特定标识符判断图片类型:

    + (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
        if (!data) {
            return SDImageFormatUndefined;
        }
        
        // File signatures table: http://www.garykessler.net/library/file_sigs.html
        uint8_t c;
        [data getBytes:&c length:1];
        switch (c) {
            case 0xFF:
                return SDImageFormatJPEG;
            case 0x89:
                return SDImageFormatPNG;
            case 0x47:
                return SDImageFormatGIF;
            case 0x49:
            case 0x4D:
                return SDImageFormatTIFF;
            case 0x52: {
                if (data.length >= 12) {
                    //RIFF....WEBP
                    NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
                    if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                        return SDImageFormatWebP;
                    }
                }
                break;
            }
            case 0x00: {
                if (data.length >= 12) {
                    //....ftypheic ....ftypheix ....ftyphevc ....ftyphevx
                    NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding];
                    if ([testString isEqualToString:@"ftypheic"]
                        || [testString isEqualToString:@"ftypheix"]
                        || [testString isEqualToString:@"ftyphevc"]
                        || [testString isEqualToString:@"ftyphevx"]) {
                        return SDImageFormatHEIC;
                    }
                    //....ftypmif1 ....ftypmsf1
                    if ([testString isEqualToString:@"ftypmif1"] || [testString isEqualToString:@"ftypmsf1"]) {
                        return SDImageFormatHEIF;
                    }
                }
                break;
            }
            case 0x25: {
                if (data.length >= 4) {
                    //%PDF
                    NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(1, 3)] encoding:NSASCIIStringEncoding];
                    if ([testString isEqualToString:@"PDF"]) {
                        return SDImageFormatPDF;
                    }
                }
            }
            case 0x3C: {
                if (data.length > 100) {
                    // Check end with SVG tag
                    NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(data.length - 100, 100)] encoding:NSASCIIStringEncoding];
                    if ([testString containsString:kSVGTagEnd]) {
                        return SDImageFormatSVG;
                    }
                }
            }
        }
        return SDImageFormatUndefined;
    }
    
    二进制图片数据解码为图片

    二进制图片解码最终实现都是调用 SDImageIOAnimatedCoder 内部实现方法:
    解码图片第一帧,或者全部帧数

    - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
        if (!data) { return nil; }
        CGFloat scale = 1;
        CGSize thumbnailSize = CGSizeZero;
        BOOL preserveAspectRatio = YES;
    
        //创建图片数据源
        CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
        if (!source) { return nil; }
        size_t count = CGImageSourceGetCount(source);
        UIImage *animatedImage;
        
        BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
        
        if (decodeFirstFrame || count <= 1) {
            //解码图片第一帧
            animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
        } else {
            
            //解码图片,遍历图片所有帧数,并合并动态图
            NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
            
            for (size_t i = 0; i < count; i++) {
                UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
                if (!image) { continue; }
                NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source];
                SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
                [frames addObject:frame];
            }
            
            NSUInteger loopCount = [self.class imageLoopCountWithSource:source];
            
            animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
            animatedImage.sd_imageLoopCount = loopCount;
        }
        animatedImage.sd_imageFormat = self.class.imageFormat;
        CFRelease(source);
        
        return animatedImage;
    }
    
    

    创建图片源的某一帧的图片

    + (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize options:(NSDictionary *)options {
        // Some options need to pass to `CGImageSourceCopyPropertiesAtIndex` before `CGImageSourceCreateImageAtIndex`, or ImageIO will ignore them because they parse once :)
        // Parse the image properties
        NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
        NSUInteger pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] unsignedIntegerValue];
        NSUInteger pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] unsignedIntegerValue];
        CGImagePropertyOrientation exifOrientation = (CGImagePropertyOrientation)[properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue];
        if (!exifOrientation) {
            exifOrientation = kCGImagePropertyOrientationUp;
        }
        
        NSMutableDictionary *decodingOptions = options ? [NSMutableDictionary dictionaryWithDictionary:options] :  [NSMutableDictionary dictionary];
        ...
        //根据图片进行图片进行尺寸、像素分辨率设置
        CGImageRef imageRef = CGImageSourceCreateThumbnailAtIndex(source, index, (__bridge CFDictionaryRef)[decodingOptions copy]);
        if (!imageRef) { return nil; }
        UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
        UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:imageOrientation];
        CGImageRelease(imageRef);
        return image;
    }
    

    图片解码为位图

    为什么要对UIImage进行解码呢?难道不能直接使用吗?
    当UIImage对象赋值给UIImageView即在屏幕上渲染图像时,首先需要对其进行解码,但这是由Core Animation在主队列上发生的。当在主线程调用了大量的图片赋值后,就会产生卡顿了。

    为了解决这个问题SDWebImage将图片解码为位图的操作放在异步线程实现,虽然需要消耗额外内存,但不会阻塞主线程。

    简单解码图片:

    1. 已经解码、动态图、矢量图都不需要解码
    2. 创建位图画布
    3. 将图片数据绘制到画布上
    + (UIImage *)decodedImageWithImage:(UIImage *)image {
        
        //已经解码、动态图、矢量图都不需要解码
        if (image == nil || image.sd_isDecoded || image.sd_isAnimated || image.sd_isVector) {
            return image;
        }
        
        //解码图片为位图
        CGImageRef imageRef = [self CGImageCreateDecoded:image.CGImage];
        if (!imageRef) { return image; }
    
        //生成uiimage,并返回图片
        UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation];
        CGImageRelease(imageRef);
        SDImageCopyAssociatedObject(image, decodedImage);
        decodedImage.sd_isDecoded = YES;
        return decodedImage;
    }
    
    + (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage {
        return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp];
    }
    
    + (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
        if (!cgImage) {
            return NULL;
        }
        
        //根据需要的图片方向,设置图片宽搞
        size_t width = CGImageGetWidth(cgImage);
        size_t height = CGImageGetHeight(cgImage);
        if (width == 0 || height == 0) return NULL;
        size_t newWidth;
        size_t newHeight;
        switch (orientation) {
            case kCGImagePropertyOrientationLeft:
            case kCGImagePropertyOrientationLeftMirrored:
            case kCGImagePropertyOrientationRight:
            case kCGImagePropertyOrientationRightMirrored: {
                // These orientation should swap width & height
                newWidth = height;
                newHeight = width;
            }
                break;
            default: {
                newWidth = width;
                newHeight = height;
            }
                break;
        }
        
        //创建没有透明因素的位图画布上下文
        CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage);
        BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                          alphaInfo == kCGImageAlphaNoneSkipFirst ||
                          alphaInfo == kCGImageAlphaNoneSkipLast);
    
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        
        //创建位图绘制上下文
        CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
        if (!context) {
            return NULL;
        }
        
        // 根据方向,对位图进行调整
        CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
        CGContextConcatCTM(context, transform);
        
        //将图片数据绘制到画布上
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height
        //生成位图
        CGImageRef newImageRef = CGBitmapContextCreateImage(context);
        CGContextRelease(context);
        
        return newImageRef;
    }
    

    解码图片,支持压缩图片(默认图片最大不得超过60M)。

    原理: 首先定义一个大小固定的方块,然后把原图按照方块的大小进行分割,最后把每个方块中的数据画到目标画布上,这样就能得到目标图像了。接下来我们做出相信的解释

    有几个点需要清楚:

    • 图像在iOS设备上是以像素为单位显示的
    • 每一个像素由RGBA组成,即R(红色)G(绿色)B(蓝色)A(透明度)4个模块,每个模块由8bit组成(16进制00~FF),所以每个像素大小 = 8bit * 4 = 32bit = 4字节
    + (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes;
    
    1. 检查图像应不应该压缩,原则是:如果图像大于目标尺寸才需要压缩,否则调用 -decodedImageWithImage返回位图
        if (![self shouldDecodeImage:image]) {
            return image;
        }
        
        if (![self shouldScaleDownImage:image limitBytes:bytes]) {
            return [self decodedImageWithImage:image];
        }
    
    + (BOOL)shouldScaleDownImage:(nonnull UIImage *)image limitBytes:(NSUInteger)bytes {
        BOOL shouldScaleDown = YES;
        
        CGImageRef sourceImageRef = image.CGImage;
        CGSize sourceResolution = CGSizeZero;
        sourceResolution.width = CGImageGetWidth(sourceImageRef);
        sourceResolution.height = CGImageGetHeight(sourceImageRef);
        
        //图片的像素总数 = 长 * 宽
        float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
        if (sourceTotalPixels <= 0) { return NO; }
        
        //压缩的图片像素大小
        CGFloat destTotalPixels = bytes = 0 ? bytes : kDestImageLimitBytes;
        destTotalPixels = bytes / kBytesPerPixel;
        if (destTotalPixels <= kPixelsPerMB) { return NO; }
        
        //图片的像素总数 vs 压缩的图片像素大小
        float imageScale = destTotalPixels / sourceTotalPixels;
        if (imageScale < 1) {
            shouldScaleDown = YES;
        } else {
            shouldScaleDown = NO;
        }
        
        return shouldScaleDown;
    }
    

    2.目标像素 destResolution

        CGFloat destTotalPixels;
        if (bytes == 0) {
            bytes = kDestImageLimitBytes;
        }
        destTotalPixels = bytes / kBytesPerPixel;
    

    其中 kBytesPerPixel为常量:每M的字节数

    static const CGFloat kBytesPerMB = 1024.0f * 1024.0f
    static CGFloat kDestImageLimitBytes = 60.f * kBytesPerMB;
    
    1. 原图分辨率sourceResolution、原图总像素 sourceResolution
    CGImageRef sourceImageRef = image.CGImage;
            
    CGSize sourceResolution = CGSizeZero;
    sourceResolution.width = CGImageGetWidth(sourceImageRef);
    sourceResolution.height = CGImageGetHeight(sourceImageRef);
    CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;
    

    4.计算图压缩比imageScale、目标图标分辨率destResolution

    CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels);
    CGSize destResolution = CGSizeZero;
    destResolution.width = (int)(sourceResolution.width * imageScale);
    destResolution.height = (int)(sourceResolution.height * imageScale);
    

    5.计算第一个原图方块 sourceTile,这个方块的宽度同原图一样,高度根据方块容量计算
    容量 = 目标总像素 / 3

    CGFloat tileTotalPixels = destTotalPixels / 3;
    
    CGRect sourceTile = CGRectZero;
    sourceTile.size.width = sourceResolution.width;
    sourceTile.size.height = (int)(tileTotalPixels / sourceTile.size.width );
    sourceTile.origin.x = 0.0f;
    

    6.计算目标图像方块 destTile

    CGRect destTile;
    destTile.size.width = destResolution.width;
    destTile.size.height = sourceTile.size.height * imageScale;
    destTile.origin.x = 0.0f;
    

    7.计算原图像块与目标方块重叠的像素大小 sourceSeemOverlap

    // 重叠的像素数量
    static const CGFloat kDestSeemOverlap = 2.0f;
    
    float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
    

    8.计算原图像需要被分割成多少个方块 iterations:
    原图分辨率高度 ➗ 原图块高度,无法整除,加1处理

    int iterations = (int)( sourceResolution.height / sourceTile.size.height );
    int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
    if(remainder) { iterations++; }
    

    9.创建位图上下文 destContext,并设置图形上下文的插值质量级别

    destContext = CGBitmapContextCreate(NULL,
                                        destResolution.width,
                                        destResolution.height,
                                        kBitsPerComponent,
                                        0,
                                        colorspaceRef,
                                        bitmapInfo);
    
    if (destContext == NULL) {
        return image;
    }
    CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
    

    10.根据重叠像素计算原图方块的大小后,获取原图中该方块内的数据,把该数据写入到相对应的目标方块中

    float sourceTileHeightMinusOverlap = sourceTile.size.height;
    sourceTile.size.height += sourceSeemOverlap;
    destTile.size.height += kDestSeemOverlap;
    for( int y = 0; y < iterations; ++y ) {
        @autoreleasepool {
            sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
            destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
            sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
            if( y == iterations - 1 && remainder ) {
                float dify = destTile.size.height;
                destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
                dify -= destTile.size.height;
                destTile.origin.y += dify;
            }
            CGContextDrawImage( destContext, destTile, sourceTileImageRef );
            CGImageRelease( sourceTileImageRef );
        }
    }
    

    11.返回目标图像

    CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
    CGContextRelease(destContext);
    if (destImageRef == NULL) { return image; }
    UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
    CGImageRelease(destImageRef);
    if (destImage == nil) { return image; }
    SDImageCopyAssociatedObject(image, destImage);
    destImage.sd_isDecoded = YES;
    return destImage;
    

    相关文章

      网友评论

        本文标题:SDWebImage源码解析

        本文链接:https://www.haomeiwen.com/subject/gfpmyftx.html