美文网首页
SDWebImage *底层探究 (二)

SDWebImage *底层探究 (二)

作者: by小杰 | 来源:发表于2016-09-14 18:56 被阅读52次

    图片加载:

    [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 *底层探究 (二)

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