iOS源码解析—SDWebImage(SDImageCache)

概述

SDWebImage是iOS开发中加载图片的库,文件目录的结构按照功能可以分为4个部分:

数据缓存类:SDImageCache

数据下载类:SDWebImageDownloader、SDWebImageDownloaderOperation

工具类:SDWebImageManager、NSData+ImageContentType、SDWebImageCompat、UIImage+GIF等

UI的category:UIImageView+WebCache、UIButton+WebCache、UIImageView+HighlightedWebCache等。

本篇学习并分析一下缓存类SDImageCache。SDImageCache提供了内存和硬盘两种方式缓存图片数据,对于内存缓存,直接将图片对象UIImage存入即可,对于硬盘缓存,先将UIImage对象序列化为NSData字节流,写入文件中。下面是SDImageCache的主要属性:

@interface SDImageCache ()
@property (strong, nonatomic) NSCache *memCache; //内存缓存
@property (strong, nonatomic) NSString *diskCachePath; //硬盘缓存
@property (strong, nonatomic) NSMutableArray *customPaths; //自定义硬盘缓存路径
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue; //串行队列,执行存储操作时的队列
@end

其中NSCache对象memCache负责将UIImage对象存入内存中,diskCachePath是存入图片数据存入硬盘的路径,customPaths是自定义的硬盘存储路径。ioQueue是串行队列,由于存储图片数据是I/O操作,在ioQueue中执行优化可以提升主线程的流畅度。

初始化方法
  1. -(id)initWithNamespace:

    - (id)initWithNamespace:(NSString *)ns {
       NSString *path = [self makeDiskCachePath:ns]; //创建文件读写路径
       return [self initWithNamespace:ns diskCacheDirectory:path]; //初始化参数
    }

    该方法首先调用makeDiskCachePath方法创建文件读写路径,在文件系统的cache目录中添加子目录,然后调用initWithNamespace:diskCacheDirectory:方法初始化参数。

  2. -(id)initWithNamespace:diskCacheDirectory:

    - (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
       if ((self = [super init])) {
           NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns]; //文件缓存目录名
           // PNG格式前缀
           kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];
           // 串行队列
           _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
           // 最大缓存期限
           _maxCacheAge = kDefaultCacheMaxCacheAge;
           // NSCache对象,用于内存缓存
           _memCache = [[AutoPurgeCache alloc] init];
           _memCache.name = fullNamespace;
    
           // 创建文件缓存目录路径
           if (directory != nil) {
               _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
           } else {
               NSString *path = [self makeDiskCachePath:ns];
               _diskCachePath = path;
           }
           //需要解压缩
           _shouldDecompressImages = YES;
           // 需要缓存到内存
           _shouldCacheImagesInMemory = YES;
           // 禁用icloud
           _shouldDisableiCloud = YES;
           dispatch_sync(_ioQueue, ^{
               _fileManager = [NSFileManager new];
           });
         ...
       return self;
    }

    该方法初始化一系列参数,主要是创建用于内存缓存的对象memCache,文件缓存的路径diskCachePath,设置标志位(需要解压缩、需要缓存内存、禁用iCloud)。初始化完成后,SDImageCache提供了存储数据、查询数据、删除数据等功能。

存储图片数据

存储数据调用的方法是-(void)storeImage:recalculateFromImage:imageData:forKey:toDisk:,代码注释如下:

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
    if (!image || !key) {
        return;
    }
    if (self.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost]; //UIImage存入内存
    }
    if (toDisk) {
        dispatch_async(self.ioQueue, ^{ //在ioQueue中执行存储逻辑
            NSData *data = imageData;
            if (image && (recalculate || !data)) {
                int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
                BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                  alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                  alphaInfo == kCGImageAlphaNoneSkipLast);
                BOOL imageIsPng = hasAlpha; //是否包含透明度信息

                // 是否包含透明度信息
                if ([imageData length] >= [kPNGSignatureData length]) {
                    imageIsPng = ImageDataHasPNGPreffix(imageData);
                }

                if (imageIsPng) { // 包含透明度信息,属于PNG格式的图片,image转成NSData
                    data = UIImagePNGRepresentation(image);
                }
                else { // 不包含透明度信息,属于JPEG格式的图片,image转成NSData
                    data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                }
            }
            [self storeImageDataToDisk:data forKey:key]; //存储数据
        });
    }
}

首先根据shouldCacheImagesInMemory判断是否先写入内存,如果为YES,则将UIImage写入memCache中,然后在ioQueue队列中将UIImage转成NSData,首先判断是否有透明度信息,通过ImageDataHasPNGPreffix方法可以判断图片是否是PNG格式,如果是PNG格式,则调用UIImagePNGRepresentation方法转换,否则用UIImageJPEGRepresentation方法转成NSData,最后调用storeImageDataToDisk:forKey:方法存储数据。storeImageDataToDisk:forKey:方法的代码注释如下:

- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key {
    if (!imageData) {
        return;
    }
    //创建文件存储目录
    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    //写入图片数据的文件路径
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    //写入图片数据imageData到文件路径中
    [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
    //禁用iCloud
    if (self.shouldDisableiCloud) {
        [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

该方法负责写入图片数据到文件中,首先判存储数据的目录是否存在,如不存在,先创建目录,然后获取写入图片数据的文件路径,调用createFileAtPath:contents:attributes:方法写入图片数据到文件中。最后调用setResourceValue:forKey:error:方法设置禁用iCloud备份。其中defaultCachePathForKey:方法的注释如下:

- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path {
    NSString *filename = [self cachedFileNameForKey:key];
    return [path stringByAppendingPathComponent:filename];
}

- (NSString *)defaultCachePathForKey:(NSString *)key {
    return [self cachePathForKey:key inPath:self.diskCachePath];
}

- (NSString *)cachedFileNameForKey:(NSString *)key {
    const char *str = [key UTF8String];
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];

    return filename;
}

该方法调用cachePathForKey:inPath:方法首先得到缓存key的md5值,然后将md5值拼接在path后面返回,作为写入图片数据的文件路径。

访问图片数据

访问图片数据的方法主要有以下几个:

  1. -(UIImage *)imageFromMemoryCacheForKey:方法,代码注释如下:

    - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
       return [self.memCache objectForKey:key]; //从内存中取数据
    }

    该方法从内存中取图片对象。

  2. -(UIImage *)imageFromDiskCacheForKey:方法,代码注释如下:

    - (UIImage *)imageFromDiskCacheForKey:(NSString *)key {
       // 首先从内存中取
       UIImage *image = [self imageFromMemoryCacheForKey:key];
       if (image) {
           return image;
       }
       // 如果内存中没有数据,则从文件中取
       UIImage *diskImage = [self diskImageForKey:key];
       if (diskImage && self.shouldCacheImagesInMemory) {
           NSUInteger cost = SDCacheCostForImage(diskImage);
           [self.memCache setObject:diskImage forKey:key cost:cost]; //数据写入内存
       }
       return diskImage;
    }

    该方法首先从内存中去图片,如果有直接返回,如果没有,则调用diskImageForKey:方法从文件中取,如果存在,则将图片数据存入内存中,最后返回。diskImageForKey:方法返回文件中写入的图片,代码注释如下:

    - (UIImage *)diskImageForKey:(NSString *)key {
       NSData *data = [self diskImageDataBySearchingAllPathsForKey:key]; //从文件中获取NSData数据
       if (data) {
           UIImage *image = [UIImage sd_imageWithData:data]; //转化为UIImage
           image = [self scaledImageForKey:key image:image]; //转化UIImage
           if (self.shouldDecompressImages) {
               image = [UIImage decodedImageWithImage:image]; //将image转化为位图image
           }
           return image;
       }
       else {
           return nil;
       }
    }

    该方法首先调用diskImageDataBySearchingAllPathsForKey:方法从文件中获取缓存数据,然后调用sd_imageWithData:方法根据data创建UIImage对象,然后调用decodedImageWithImage:方法将image转成位图格式的image。该方法代码注释如下:

    + (UIImage *)decodedImageWithImage:(UIImage *)image {
       if (image == nil) {
           return nil;
       }
    
       @autoreleasepool{
           if (image.images != nil) {
               return image;
           }
    
           CGImageRef imageRef = image.CGImage;
           //如果有alpha信息,则不转化,直接返回image
           CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
           BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
                            alpha == kCGImageAlphaLast ||
                            alpha == kCGImageAlphaPremultipliedFirst ||
                            alpha == kCGImageAlphaPremultipliedLast);
           if (anyAlpha) {
               return image;
           }
           //获取图像的相关参数
           CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
           CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
    
           BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
                                         imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
                                         imageColorSpaceModel == kCGColorSpaceModelCMYK ||
                                         imageColorSpaceModel == kCGColorSpaceModelIndexed);
           if (unsupportedColorSpace) {
               colorspaceRef = CGColorSpaceCreateDeviceRGB();
           }
           size_t width = CGImageGetWidth(imageRef);
           size_t height = CGImageGetHeight(imageRef);
           NSUInteger bytesPerPixel = 4;
           NSUInteger bytesPerRow = bytesPerPixel * width;
           NSUInteger bitsPerComponent = 8;
    
           //创建context
           CGContextRef context = CGBitmapContextCreate(NULL,
                                                        width,
                                                        height,
                                                        bitsPerComponent,
                                                        bytesPerRow,
                                                        colorspaceRef,
                         kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
    
           // 画图像
           CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
           //获取位图图像
           CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
            //创建UIImage对象
           UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha scale:image.scale                                            orientation:image.imageOrientation];
    
           if (unsupportedColorSpace) {
               CGColorSpaceRelease(colorspaceRef);
           }
           CGContextRelease(context);
           CGImageRelease(imageRefWithoutAlpha);
           return imageWithoutAlpha;
       }
    }

    该方法首先判断图片是否包含alpha信息,如果包含则不转化直接返回。如果不包含则获取相关参数创建上下文context对象,然后绘制位图,并调用CGBitmapContextCreateImage方法取出位图对象,最后调用imageWithCGImage:scale:orientation:方法生成UIImage对象并返回。imageRefWithoutAlpha是位图,宽和高都是图像的真是像素值,而imageWithoutAlpha对象则包含scale和imageOrientation信息,宽和高是根据scale比例缩放的宽和高。例如一张@2x的图片,像素宽和高是512和256,在retina屏幕上的imageWithoutAlpha对象的宽和高是256和128,scale是2。imageRefWithoutAlpha的宽和高是512和256。

    之所以需要调用decodedImageWithImage:方法的原因是,通常APP加载的图片源是PNG或者JPEG格式,当调用图片控件UIImageView加载图片的时候,首先需要将其转成位图格式,然后渲染在屏幕上。预先在子线程中进行转化,可以使UIImageView直接加载位图,提升图片加载的性能。

  3. -(NSOperation *)queryDiskCacheForKey: done: 方法

    该方法通过block的方式异步返回缓存的图片,首先从内存中获取图片,如果有则直接回调给上层。如果没有,再从文件中获取图片,如能获取到,则写入内存,同时将获取到图片回调给上层。代码注释如下:

    - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
       if (!doneBlock) {
           return nil;
       }
       if (!key) {
           doneBlock(nil, SDImageCacheTypeNone);
           return nil;
       }
       // 从内存中获取图片
       UIImage *image = [self imageFromMemoryCacheForKey:key];
       if (image) {
           doneBlock(image, SDImageCacheTypeMemory); //能够取到,直接回调给上层
           return nil;
       }
       NSOperation *operation = [NSOperation new];
       dispatch_async(self.ioQueue, ^{
           if (operation.isCancelled) {
               return;
           }
           @autoreleasepool {
               UIImage *diskImage = [self diskImageForKey:key]; //从文件中获取图片
               if (diskImage && self.shouldCacheImagesInMemory) {
                   NSUInteger cost = SDCacheCostForImage(diskImage); //写入内存
                   [self.memCache setObject:diskImage forKey:key cost:cost];
               }
               dispatch_async(dispatch_get_main_queue(), ^{
                   doneBlock(diskImage, SDImageCacheTypeDisk); //将图片回调给上层
               });
           }
       });
       return operation;
    }
删除图片缓存数据
  1. -(void)removeImageForKey:fromDisk:withCompletion:方法

    该方法删除key对应的缓存数据,分别从内存和文件中删除,代码注释如下:

    - (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion {
       if (key == nil) {
           return;
       }
       if (self.shouldCacheImagesInMemory) {
           [self.memCache removeObjectForKey:key]; //从内存中删除图片数据
       }
       if (fromDisk) {
           dispatch_async(self.ioQueue, ^{
               [_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil]; //从文件中删除图片数据
               if (completion) {
                   dispatch_async(dispatch_get_main_queue(), ^{
                       completion();
                   });
               }
           });
       } else if (completion){
           completion();
       }
    }
  2. -(void)clearDiskOnCompletion:方法

    该方法通过删除目录的方式删除文件中所有缓存的数据,并重新缓存目录,代码注释如下:

    - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion
    {
       dispatch_async(self.ioQueue, ^{
           [_fileManager removeItemAtPath:self.diskCachePath error:nil]; //删除存放图片数据的目录
           [_fileManager createDirectoryAtPath:self.diskCachePath
                   withIntermediateDirectories:YES
                                    attributes:nil
                                         error:NULL]; //重新创建目录
           if (completion) {
               dispatch_async(dispatch_get_main_queue(), ^{
                   completion();
               });
           }
       });
    }
  3. -(void)cleanDiskWithCompletionBlock:

    该方法根据缓存期限和缓存容量,删除文件中的部分图片数据。代码注释如下:

    - (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
       dispatch_async(self.ioQueue, ^{
           NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
           NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
    
           NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL includingPropertiesForKeys:resourceKeys                                                                    options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL];
        //缓存期限
           NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
           NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
           NSUInteger currentCacheSize = 0;
    
           NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
           for (NSURL *fileURL in fileEnumerator) {
               NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
            //如果是目录,忽略
               if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                   continue;
               }
            //将过期的缓存数据加入urlsToDelete数组中
               NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
               if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                   [urlsToDelete addObject:fileURL];
                   continue;
               }
            //计算没有过期的缓存数据的大小
               NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
               currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
               [cacheFiles setObject:resourceValues forKey:fileURL];
           }
           //删除过期的缓存数据
           for (NSURL *fileURL in urlsToDelete) {
               [_fileManager removeItemAtURL:fileURL error:nil];
           }
    
           //判断没有过期的缓存数据大小是否大于最大缓存容量
           if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
               //如果大于,则设置缓存最大容量的1/2为预留空间大小desiredCacheSize
               const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
    
               //根据修改日期排序
               NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent                                                           usingComparator:^NSComparisonResult(id obj1, id obj2) {
                 return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];}];
    
               // 删除缓存数据,直到缓存数据总大小小于desiredCacheSize
               for (NSURL *fileURL in sortedFiles) {
                   if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                       NSDictionary *resourceValues = cacheFiles[fileURL];
                       NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                       currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
                       if (currentCacheSize < desiredCacheSize) {
                           break;
                       }
                   }
               }
           }
           if (completionBlock) {
               dispatch_async(dispatch_get_main_queue(), ^{
                   completionBlock();
               });
           }
       });
    }

    一、该方法遍历文件目录下的所有缓存图片数据,将缓存日期超过最大期限expirationDate的数据删除,同时计算剩余数据的总大小。

    二、判断剩余图片数据的总大小是否大于最大容量maxCacheSize,如果超过,则保留maxCacheSize的1/2空间desiredCacheSize,其余数据删除,具体做法是对剩余图片数据按照修改日期排序,逐个删除,直到总大小小于desiredCacheSize为止。

其他方法

SDWebImage还提供了一些工具方法例如:

  1. -(NSUInteger)getSize方法,用于同步获取缓存在文件中图片数据总大小。

  2. -(NSUInteger)getDiskCount方法,用于同步获取缓存在文件中图片数据的个数。

  3. -(void)calculateSizeWithCompletionBlock:方法,用于异步获取缓存在文件中图片数据总大小。

  4. -(void)backgroundCleanDisk方法,用于在程序进入后台的时候删除超过期限和总大小的图片数据。代码注释如下:

    - (void)backgroundCleanDisk {
       Class UIApplicationClass = NSClassFromString(@"UIApplication");
       if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
           return;
       }
       UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
       __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
           [application endBackgroundTask:bgTask]; //如果超时,则终止task
           bgTask = UIBackgroundTaskInvalid;
       }];
       //根据缓存期限和缓存容量,删除文件中的部分图片数据
       [self cleanDiskWithCompletionBlock:^{
           [application endBackgroundTask:bgTask];
           bgTask = UIBackgroundTaskInvalid;
       }];
    }

    该方法调用系统方法UIApplication的beginBackgroundTaskWithExpirationHandler方法开启一个用于后台执行任务的task,并且设置超时的block。该方法允许在程序进入后台的时候提供一段时间用于执行app未完成的任务,如果超出这段时间,则执行ExpirationHandler的block,调用endBackgroundTask:方法终止task。cleanDiskWithCompletionBlock方法是在后台执行的代码,执行完成后会调用endBackgroundTask:方法终止task。

总结

SDImageCache同时使用内存和文件来缓存数据,设计思路值得学习和借鉴。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值