SDWebImage *底层探究 (二)

图片加载:

[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] 
                  placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
# 其实是通过 SDWebImageManager类进行协调,调用 SDImageCache与 SDWebImageDownloader来实现图片的缓存查询与网络下载的。

1. SDImageCache

该类维护了一个内存缓存与一个可选的磁盘缓存. 同时, 磁盘缓存的写操作是异步的, 所以他不会对UI造成不必要的影响.

*每次查询图片时, 首先会根据图片的URL对应的key值检测内存中是否有对应的图片:
@ 如果有则直接返回;
@ 如果没有则在ioQueue中去磁盘中查找;
其key是根据URL生成的MD5值, 找到图片缓存在内存中, 然后把图片返回.

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
     if (!doneBlock) { 
            return nil; 
     }
     if (!key) { 
            doneBlock(nil, SDImageCacheTypeNone); 
            return nil; 
     } 

    // 首先检查内存缓存(查询是同步的),如果查找到,则直接回调 doneBlock 并返回 
    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 { 
        // 检查磁盘缓存(查询是异步的),如果查找到,则将其放到内存缓存,并调用 doneBlock 回调
        UIImage *diskImage = [self diskImageForKey:key]; 
        if (diskImage) { 
            NSUInteger cost = SDCacheCostForImage(diskImage); 
            // 缓存至内存(NSCache)中 
            [self.memCache setObject:diskImage forKey:key cost:cost];
        } 
        // 返回主线程设置图片 
        dispatch_async(dispatch_get_main_queue(), ^{ 
            doneBlock(diskImage, SDImageCacheTypeDisk); 
        });
     }
   }); 
  return operation;
}

2. NSCache

NSCache 是苹果官方提供的缓存类,用法与 NSMutableDictionary 的用法很相似,在 SDWebImage 和 AFNetworking 中,使用它来管理缓存。同样是以 key-value 的形式进行存储,那么 NSCache 与 NSMutableDictionary 等集合类的区别或者说优势又是哪些呢?

  • NSCache 类结合了各种自动删除策略,以确保不会占用过多的系统内存。如果其它应用需要内存时,系统自动执行这些策略。当调用这些策略时,会从缓存中删除一些对象,以最大限度减少内存的占用
  • NSCache 是线程安全的,我们可以在不同的线程中添加、删除和查询缓存中的对象,而不需要锁定缓存区域
  • 不像 NSMutableDictionary 对象,NSCache 对象并不会拷贝键(key),而是会强引用它

要点

  1. 在开发者自己编写加锁代码的前提下, 多个线程便可以同时访问NSCache
  2. NSCache对象不拷贝键的原因在于: 很多时候, 键都是由不支持拷贝操作的对象来充当的. 所以说, 在不支持拷贝操作的情况下, 该类用起来比字典更方便.
  3. 可以给NSCache对象设置上限, 用以限制缓存中的对象总个数, 而这些尺度则定义了缓存删减中对象的时间. 但是绝对不要把这些尺度当成靠山, 他们仅对于NSCache起指导作用.
  4. 将NSPurgeableData与NSCache搭配使用, 可实现自动清除数据的功能, 也就是说, 当NSPurgeableData对象所占内存为系统所丢弃时, 该对象自身也会从缓存中移除.
  5. 如果缓存使用得当, 那么应用程序的响应速度就能提高. 只有那种(重新计算起来哼费时的)数据, 才值得放入缓存, 比如那些需要从网络获取或者从磁盘读取的数据.
  6. 内存查询是同步, 磁盘查询是异步.

3. 磁盘

磁盘缓存的处理则是使用NSFileManager对象来实现的. 默认以com.hackemist.SDWebImageCache.default为磁盘的缓存命名空间, 程序运行后, 可以在程序的文件夹Library/Caches/default/com.hackemist.SDWebImageCache.default下看到一些缓存文件. 另外, SDImageCache还定义了一个串行队列, 来异存储图片.

在磁盘查询的时候, 会在后台将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;
    }
}

4. 存储图片

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

- (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) { 
         // 如果确定需要磁盘缓存,则将缓存操作作为一个任务放入 ioQueue 中 
         dispatch_async(self.ioQueue, ^{ 
            // 构建一个 data,用来存储到 disk 中,默认值为 imageData 
            NSData *data = imageData; 
            if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
                 // 需要确定图片是 PNG 还是 JPEG。PNG 图片容易检测,因为有一个唯一签名。PNG 图像的前 8 个字节总是包含以下值:137 80 78 71 13 10 26 10 // 在 imageData 为 nil 的情况下假定图像为 PNG。我们将其当作 PNG 以避免丢失透明度。而当有图片数据时,我们检测其前缀,确定图片的类型 
                 BOOL imageIsPng = YES; 
                 if ([imageData length] >= [kPNGSignatureData length]) { 
                     imageIsPng = ImageDataHasPNGPreffix(imageData); 
                 }
                // 如果 image 是 PNG 格式,就是用 UIImagePNGRepresentation 将其转化为 NSData,否则按照 JPEG 格式转化,并且压缩质量为 1,即无压缩 
                if (imageIsPng) { 
                     data = UIImagePNGRepresentation(image); 
                } else { 
                    data = UIImageJPEGRepresentation(image, (CGFloat)1.0); 
                }
#else
               data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif 
        } 
        // 创建缓存文件并存储图片(使用 fileManager) 
        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]; 
        } 
    }); 
  }
}

5. 清理图片

SDImageCache 会在系统发出低内存警告时释放内存,并且在程序进入 UIApplicationWillTerminateNotification 时,清理磁盘缓存,清理磁盘的机制是:

  1. 删除过期的图片,默认 7 天过期,可以通过 maxCacheAge 修改过期天数。

  2. 如果缓存的数据大小超过设置的最大缓存 maxCacheSize,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于我们设置的最大使用空间,可以通过修改 maxCacheSize 来改变最大缓存大小。

- (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; } 
        // 移除早于有效期的老文件 
        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) { 
        // 以设置的最大缓存大小的一半作为清理目标 
        const NSUInteger desiredCacheSize = self.maxCacheSize / 2; 
        // 按照最后修改时间来排序剩下的缓存文件
         NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(id obj1, id obj2) { 
            return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
        }]; 

       // 删除文件,直到缓存总大小降到我们期望的大小 
       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(); }); 
  }
 });
}

http://itangqi.me/2016/03/23/the-notes-of-learning-sdwebimage-three/
http://itangqi.me/2016/03/24/the-notes-of-learning-sdwebimage-four/

你可能感兴趣的:(SDWebImage *底层探究 (二))