美文网首页
读SDWebImage源码记录(二)

读SDWebImage源码记录(二)

作者: 一剑书生 | 来源:发表于2015-06-20 23:54 被阅读411次

    接着上一篇,继续分析SDWebImage源码。上一篇主要分析了SDWebImage是怎么把图片下载到本地的,这一篇主要分析它怎么做缓存管理的。


    基本思路

    1, 下载图片前,先根据图片URL,检查缓存中(内存与磁盘)是否有该URL对应的图片;
    2, 下载图片成功后,将图片缓存到内存中,并写到磁盘中;若失败,将该URL列入失败无效URL中


    主要相关类

    1,SDImageCache :图片缓存器,专门负责图片缓存
    2,SDWebImageDownloader:图片下载器,专门负责图片下载
    3,SDWebImageManager:图片管理类,负责调度SDWebImageDownloader和SDImageCache,下载图片前,使用SDImageCache判断图片是否缓存,若无缓存则派SDWebImageDownloader上场,去下载图片。


    回到SDWebImageManager的主要方法:

    - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                             options:(SDWebImageOptions)options
                                            progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
    
        if ([url isKindOfClass:NSString.class]) {
            url = [NSURL URLWithString:(NSString *)url];
        }
        if (![url isKindOfClass:NSURL.class]) {
            url = nil;
        }
    
        __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
        __weak SDWebImageCombinedOperation *weakOperation = operation;
    
        BOOL isFailedUrl = NO;
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url]; //是否虚假无效的url
        }
        //url不存在 或者 虚假且标志位为不打算重新尝试下载
        if (!url || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
            dispatch_main_sync_safe(^{
                NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
                completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
            });
            return operation;
        }
    .......
        NSString *key = [self cacheKeyForURL:url];
    
        operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
            ........省略.....
    
    id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                   //以下代码,下载结束后执行:
                    if (weakOperation.isCancelled) {
                    }
                    else if (error) { //----下载图片失败后----
                        dispatch_main_sync_safe(^{
                            if (!weakOperation.isCancelled) {
                                completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
                            }
                        });
    
                        if (error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut) {
                            @synchronized (self.failedURLs) {
                                [self.failedURLs addObject:url];
                            }
                        }
                    }
               ......下载图片成功后的处理......
               else {
                            if (downloadedImage && finished) { //保存到磁盘
                                [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; 
                            }
    
                            dispatch_main_sync_safe(^{
                                if (!weakOperation.isCancelled) {
                                    completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
                                }
                            });
                        }
             }
          .............
    }
    

    以上代码可以看到,下载图片前,先判断URL地址是否在failedURLs中,若是虚假无效的URL,则在主线中回调completedBlock;下载图片失败后,如果不是因为网络异常的原因,则判断该URL失败无效,添加到failedURLs的集合中;

    另外,可以看到以URL作为key,self.imageCache调用了queryDiskCacheForKey:done:doneBlock 方法进行了缓存查询,跳进去看看SDImageCache中的这个方法:

    - (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) {
                    NSUInteger cost = SDCacheCostForImage(diskImage);
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
    
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, SDImageCacheTypeDisk);
                });
            }
        });
    
        return operation;
    }
    

    以上代码可以看到,根据key查询内存中是否有对应图片,若无则异步读取磁盘中的图片。其中,获取内存中缓存的图片,使用了以下方法:

    - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
        return [self.memCache objectForKey:key];
    }
    

    self.memCache 其实是NSCache,NSCache是一个类似于集合的容器,存储键值对,与NSDictionary有点相似,但是它的特别在于:在系统资源将要耗尽时,它会自动移除缓存,并会优先移除最久未使用的对象。而且,NSCache是线程安全的,所以你看不到对NSCache有加锁的操作。
    self.memCache 在 SDImageCache类初始化中被创建:

    - (id)init {
        return [self initWithNamespace:@"default"];
    }
    - (id)initWithNamespace:(NSString *)ns {
        if ((self = [super init])) {
            NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns]; //用于目录空间名
            // 初始化 PNG 签名数据
            kPNGSignatureData = [NSData dataWithBytes:kPNGSignatureBytes length:8];
    
            // 创建IO串行队列
            _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
    
            // 缓存过期时间,默认是一周
            _maxCacheAge = kDefaultCacheMaxCacheAge;
    
            // 初始化缓存
            _memCache = [[NSCache alloc] init];
            _memCache.name = fullNamespace;
    
            // 硬盘缓存路径
            _diskCachePath = [self makeDiskCachePath:fullNamespace];
    
            // Set decompression to YES
            _shouldDecompressImages = YES;
    
            dispatch_sync(_ioQueue, ^{
                _fileManager = [NSFileManager new];
            });
    
    #if TARGET_OS_IPHONE
            // 内存不足发出警告时,清除内存中的缓存
            [[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];
    #endif
        }
    
        return self;
    }
    

    看完初始化方法,再来看看它是如何把图片写到磁盘上去的:

    - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
        if (!image || !key) {
            return;
        }
        //获取图片的大小
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    
        if (toDisk) {
            dispatch_async(self.ioQueue, ^{
                NSData *data = imageData;
    
                if (image && (recalculate || !data)) {
    #if TARGET_OS_IPHONE
                    
                    BOOL imageIsPng = YES;
    
                    // But if we have an image data, we will look at the preffix
                    if ([imageData length] >= [kPNGSignatureData length]) {
                        imageIsPng = ImageDataHasPNGPreffix(imageData);
                    }
    
                    if (imageIsPng) {
                        data = UIImagePNGRepresentation(image);
                    }
                    else {
                        data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                    }
    #else
                    data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
    #endif
                }
    
                if (data) {
                    if (![_fileManager fileExistsAtPath:_diskCachePath]) { //缓存目录不存在,则创建
                        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
                    }
                    //将数据写入文件中
                    [_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
                }
            });
        }
    }
    

    以上代码中,先讲图片保存到内存到缓存中,然后异步将图片数据写到磁盘文件中,使用的是NSFileManager对象中的方法。
    至此,基本上了解了SDWebImage的缓存管理机制。

    以上个人见解,水平有限,如有错漏,欢迎指出,就酱~~~

    相关参考

    阅读SDWebImage源码
    SDWebImage源码分析
    《Objective-C高级编程》一书

    相关文章

      网友评论

          本文标题:读SDWebImage源码记录(二)

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