SDWebImage源码剖析(二)

作者: 树下老男孩 | 来源:发表于2015-05-10 12:21 被阅读4772次

    SDWebImageCache管理着SDWebImage的缓存,其中内存缓存采用NSCache,同时会创建一个ioQueue负责对硬盘的读写,并且会添加观察者,在收到内存警告、关闭或进入后台时完成对应的处理:

    - (id)init {
         _memCache = [[NSCache alloc] init];
         _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
         //收到内存警告时,清除NSCache:[self.memCache removeAllObjects];
         [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(clearMemory)
                                                  name:UIApplicationDidReceiveMemoryWarningNotification
                                                       object:nil];
          //程序关闭时,会对硬盘文件做一些处理
          [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(cleanDisk)
                                                         name:UIApplicationWillTerminateNotification
                                                       object:nil];
          //程序进入后台时,也会进行硬盘文件处理
          [[NSNotificationCenter defaultCenter] addObserver:self
                                                     selector:@selector(backgroundCleanDisk)
                                                         name:UIApplicationDidEnterBackgroundNotification
                                                       object:nil];
    }
    

    查询图片

    每次向SDWebImageCache索取图片的时候,会先根据图片URL对应的key值先检查内存中是否有对应的图片,如果有则直接返回;如果没有则在ioQueue中去硬盘中查找,其中文件名是是根据URL生成的MD5值,找到之后先将图片缓存在内存中,然后在把图片返回:

    - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
        /*...*/
        // 首先查找内存缓存
        UIImage *image = [self imageFromMemoryCacheForKey:key];
        if (image) {
            doneBlock(image, SDImageCacheTypeMemory);
            return nil;
        }
        //硬盘查找
        NSOperation *operation = [NSOperation new];
        dispatch_async(self.ioQueue, ^{
            //创建自动释放池,内存及时释放
            @autoreleasepool {
                UIImage *diskImage = [self diskImageForKey:key];
                if (diskImage) {
                    CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale * diskImage.scale;
                    //缓存到NSCache中
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, SDImageCacheTypeDisk);
                });
            }
        });
        return operation;
    }
    
    

    在硬盘查询的时候,会在后台将NSData转成UIImage,并完成相关的解码工作:

    - (UIImage *)diskImageForKey:(NSString *)key {
        NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
        if (data) {
            UIImage *image = [UIImage sd_imageWithData:data];
            image = [self scaledImageForKey:key image:image];
            if (self.shouldDecompressImages) {
                image = [UIImage decodedImageWithImage:image];
            }
            return image;
        }
        else {
            return nil;
        }
    }
    

    保存图片

    当下载完图片后,会先将图片保存到NSCache中,并把图片像素大小作为该对象的cost值,同时如果需要保存到硬盘,会先判断图片的格式,PNG或者JPEG,并保存对应的NSData到缓存路径中,文件名为URL的MD5值:

    - (NSString *)cachedFileNameForKey:(NSString *)key {
        //根据key生成对应的MD5值作为文件名
        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]];
    
        return filename;
    }
    
    - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk 
    {
        //保存到NSCache,cost为像素值
        [self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale * image.scale];
        if (toDisk) {
            dispatch_async(self.ioQueue, ^{
                NSData *data = imageData;
                if (image && (recalculate || !data)) {
                   //判断图片格式
                    BOOL imageIsPng = YES;
                    // 查看imagedata的前缀是否是PNG的前缀格式
                    if ([imageData length] >= [kPNGSignatureData length]) {
                        imageIsPng = ImageDataHasPNGPreffix(imageData);
                    }
                    if (imageIsPng) {
                        data = UIImagePNGRepresentation(image);
                    }
                    else {
                        data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                    }
                }
                if (data) {
                    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
                        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                    }
                    //保存data到指定的路径中
                    [_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
                }
            });
        }
    }
    

    硬盘文件的管理

    在程序退出或者进入后台时,会出图片文件进行管理,具体的策略:

    • 清除过期的文件,默认一星期
    • 如果设置了最大缓存,并且当前缓存的文件超过了这个限制,则删除最旧的文件,直到当前缓存文件的大小为最大缓存大小的一半
    - (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
        dispatch_async(self.ioQueue, ^{
            NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
            NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
    
            // This enumerator prefetches useful properties for our cache files.
            NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                       includingPropertiesForKeys:resourceKeys
                                                                          options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                     errorHandler:NULL];
    
            NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
            NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
            NSUInteger currentCacheSize = 0;
    
            // Enumerate all of the files in the cache directory.  This loop has two purposes:
            //
            //  1. Removing files that are older than the expiration date.
            //  2. Storing file attributes for the size-based cleanup pass.
            NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
            for (NSURL *fileURL in fileEnumerator) {
                NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
    
                // Skip directories.
                if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
                    continue;
                }
    
                // Remove files that are older than the expiration date;
                NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
                if ([[modificationDate 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 setObject:resourceValues forKey:fileURL];
            }
            
            for (NSURL *fileURL in urlsToDelete) {
                [_fileManager removeItemAtURL:fileURL error:nil];
            }
    
            // If our remaining disk cache exceeds a configured maximum size, perform a second
            // size-based cleanup pass.  We delete the oldest files first.
            if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
                // Target half of our maximum cache size for this cleanup pass.
                const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
    
                // Sort the remaining cache files by their last modification time (oldest first).
                NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                    return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                }];
    
                // Delete files until we fall below our desired cache size.
                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();
                });
            }
        });
    }
    

    总结

    • 接口设计简单
      通常我们使用较多的UIImageView分类:
    [self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]
                      placeholderImage:[UIImage imageNamed:@"placeholder"]];
    

    一个简单的接口将其中复杂的实现细节全部隐藏:简单就是美。

    • 采用NSCache作为内存缓
    • 耗时较长的请求,都采用异步形式,在回调函数块中处理请求结果
    • NSOperation和NSOperationQueue:可以取消任务处理队列中的任务,设置最大并发数,设置operation之间的依赖关系。
    • 图片缓存清理的策略
    • dispatch_barrier_sync:前面的任务执行结束后它才执行,而且它后面的任务要等它执行完成之后才会执行。
    • 使用weak self strong self 防止retain circle
    • 如果子线程进需要不断处理一些事件,那么设置一个Run Loop是最好的处理方式

    相关文章

      网友评论

      • AbnerZhang:忘记说 了, 是遍历这个数组, 取出url执行这个方法, 会有completed 的block不执行的问题
      • AbnerZhang:你好, 我在用SD给UIImageView设置图片时遇到了一些问题, 希望有时间帮忙解答,有一下情况发生: 有一个保存远程图片url的数组, 我现在用sd_setImageWithURL:[NSURL URLWithString:<#(nonnull NSString *)#>] placeholderImage:<#(UIImage *)#> completed:<#^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL)completedBlock#>这个方法加载图片, 但是打断点后completed这个block会有不执行的情况发生.
        ducks:是不是图片已经下载了

      本文标题:SDWebImage源码剖析(二)

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