SDWebImage源码解析

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;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269