一问一答看SDWebImage源码

怎样进行图片下载?

图片下载真正的动作是在这里

//SDWebImageDownloader.m
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
    __block SDWebImageDownloaderOperation *operation;
    __weak __typeof(self)wself = self;
    //addProgressCallback...方法主要是将progressBlock(过程回调)和completedBlock(结束回调)保存起来。
    //以url为key保存到SDWebImageDownloader的URLCallbacks里面,供后面使用。
    [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
    NSTimeInterval timeoutInterval = wself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (wself.headersFilter) {
            request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = wself.HTTPHeaders;
        }
        operation = [[wself.operationClass alloc] initWithRequest:request
                                                        inSession:self.session
                                                          options:options
                                                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {
                                                          //过程回调
                                                         }
                                                        completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
                                                          //结束回调
                                                         }
                                                        cancelled:^{
                                                          //取消回调
                                                        }
                    ];
        operation.shouldDecompressImages = wself.shouldDecompressImages;
        
        if (wself.urlCredential) {
            operation.credential = wself.urlCredential;
        } else if (wself.username && wself.password) {
            operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }

        [wself.downloadQueue addOperation:operation];
        if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            [wself.lastAddedOperation addDependency:operation];
            wself.lastAddedOperation = operation;
        }
    }];
    return operation;
}

从代码可以看出,先是创建了一个SDWebImageDownloaderOperation的实例operation,然后把它添加到下载队列downloadQueue中。SDWebImageDownloaderOperation继承于NSOperation,重写了start方法,downloadQueue在添加后会尽快开始执行,去调start方法。(NSOperation介绍
紧接着SD下载图片是通过NSURLSession和NSURLSessionTask配合来完成的。NSURLSession是在SDWebImageDownloader里面实例化的,然后传入给SDWebImageDownloaderOperation,作为它的一个属性,叫session。然后session用来实例化dataTask。(为了防止到这里session还没初始化,所以在里面又做了一层判断,如果还没初始化,那么我就初始化一个,并且跟外面传进来的区分开,用完后也由我自己释放。)之后启动任务开始下载图片。(NSURLSession介绍

//代码节选自SDWebImageDownloaderOperation.m
// This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run
// the task associated with this operation
@property (weak, nonatomic) NSURLSession *unownedSession;
// This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
@property (strong, nonatomic) NSURLSession *ownedSession;

- (void)start {
//.......
        NSURLSession *session = self.unownedSession;
        if (!self.unownedSession) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            session = self.ownedSession;
        }
        self.dataTask = [session dataTaskWithRequest:self.request];
//.......
        [self.dataTask resume];
//.......
}

- (void)reset {
    self.cancelBlock = nil;
    self.completedBlock = nil;
    self.progressBlock = nil;
    self.dataTask = nil;
    self.imageData = nil;
    self.thread = nil;
    if (self.ownedSession) {
        [self.ownedSession invalidateAndCancel];
        self.ownedSession = nil;
    }
}

那如何取消图片下载呢?

从上文看,取消下载图片的线程只要调用SDWebImageDownloader- (void)cancelAllDownloads方法,这个方法会调用downloadQueuecancelAllOperations方法取消。

SDWebImageManager的runningOperations干嘛用的?

runningOperations是一个数组,里面的元素是SDWebImageCombinedOperation,这个类直接继承NSObject,实现了SDWebImageOperation(只是为了拥有一个cancel方法)。

@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>

@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic) NSOperation *cacheOperation;

@end

runningOperations用于标示应用目前有多少个正在获取图片的操作(记住,不是NSOperation,也不是下载图片这个动作本身)。当用户所有正在获取图片的操作都不想要了的情况,可以调用- (void)cancelAll方法,这个方法会对runningOperations里面的子元素都执行cancel方法,之后清空这个数组。

//SDWebImageCombinedOperation
- (void)cancel {
    self.cancelled = YES;
    if (self.cacheOperation) {
        [self.cacheOperation cancel];
        self.cacheOperation = nil;
    }
    if (self.cancelBlock) {
        self.cancelBlock();
        
        // TODO: this is a temporary fix to #809.
        // Until we can figure the exact cause of the crash, going with the ivar instead of the setter
//        self.cancelBlock = nil;
        _cancelBlock = nil;
    }
}

什么?这里也有个取消?那跟上面讲的取消图片下载有什么关系?

我们看下面代码

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
  //...
  __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
  __weak SDWebImageCombinedOperation *weakOperation = operation;
  //....
  @synchronized (self.runningOperations) {
    [self.runningOperations addObject:operation];
  }
  //...
  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) {//....}];
  operation.cancelBlock = ^{
    [subOperation cancel];
    
    @synchronized (self.runningOperations) {
        __strong __typeof(weakOperation) strongOperation = weakOperation;
        if (strongOperation) {
            [self.runningOperations removeObject:strongOperation];
        }
    }
  };
  //....

  return operation;
}

operation.cacheOperation其实是获取缓存的一个NSOperation,确切的说应该是从磁盘获取图片,但是在这里并没有像图片下载使用到的一样,这里只是作为一个是否取消的标示而已。当调用- (void)cancelAll方法时,operation.cacheOperation的取消是为了取消缓存图片的获取。
但是注意到这里还有一个operation.cancelBlock。调用- (void)cancelAll方法是会执行cancelBlock的。那这个是干嘛的。
上面已经讲到,下载图片的operation是由SDWebImageDownloader- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock方法返回的,返回的operation恰好是给operation.cancelBlock里面调用。cancelBlock执行时,就直接把下载图片的operation给取消了。
所以SDWebImageManager调用- (void)cancelAll后,1会取消从磁盘加载缓存图片,2会取消图片下载动作。

缓存如何搜索到?

缓存搜索是在SDImageCache- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock方法中进行的,我们来看下代码

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

    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // First check the in-memory cache...
    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;
}

从内存获取缓存是指从系统提供的NSCache的一个对象memCache获取。内存缓存这里使用的不是集合,我想应该是NSCache可以设置totalCostLimitcountLimit,这两个属性可以在内存管理更加自动化些。(NSCache介绍
从磁盘获取缓存的时候是先用NSOperation实例化了一个operation对象,operation传给外面,用于控制磁盘获取是否取消。磁盘取到对象后会根据缓存策略决定是否将图片保存到内存中。

如何从磁盘找到缓存的图片?

看下面这个方法

- (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;
    }
}

上面方法获取到data之后,转为image,并根据是否解压设置image。重点的还是下面这个方法

- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath];
    if (data) {
        return data;
    }

    // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
    // checking the key with and without the extension
    data = [NSData dataWithContentsOfFile:[defaultPath stringByDeletingPathExtension]];
    if (data) {
        return data;
    }

    NSArray *customPaths = [self.customPaths copy];
    for (NSString *path in customPaths) {
        NSString *filePath = [self cachePathForKey:key inPath:path];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath];
        if (imageData) {
            return imageData;
        }

        // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
        // checking the key with and without the extension
        imageData = [NSData dataWithContentsOfFile:[filePath stringByDeletingPathExtension]];
        if (imageData) {
            return imageData;
        }
    }

    return nil;
}

从方法名就可以知道是查找所有的路径,找到data后返回。这些路径包括defaultPathcustomPathsdefaultPath可以自定义设置,如果没有系统会默认创建。值得一提的是为了确保data的唯一性,SD使用CC_MD5的方式对key做了加密,然后用来作为文件的名字。

为毛可以控制到图片的下载进度呢?

上文提到,SD下载图片是通过NSURLSessionNSURLSessionTask配合来完成的。进度的控制归功于以下的方法

//response带有目标文件的大小,可以从这个里面获取到。
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler;
//只要接收到数据,就会调用这个方法,所以这个方法是重复调用的。
//可以从这里获取到单次接收了多少,然后保存到内存变量imageData,这样就知道已经接收到了多少。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data;

有了总量和单次接收量自然可以知道进度,这时候再调用回调处理就可以了。

SD的内存和性能处理?

内存:进入后台时/程序退出(UIApplicationWillTerminateNotification),会对过期图片(什么是过期图片?就是已经缓存超过maxCacheAge时间的那些图片)进行删除。删除之后如果剩下图片占有的大小大于最大大小(maxCacheSize)的一半,那么会根据图片修改时间排序后,删除旧图片,直到大小满足。当程序收到内存报警时,将内存都删掉。详细代码见- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock方法。
性能:图片的下载都是开新的异步线程。

//来源SDImageCache.m
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(clearMemory)
                                             name:UIApplicationDidReceiveMemoryWarningNotification
                                           object:nil];
//下面两个方法都会调用
//- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock方法
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(cleanDisk)
                                             name:UIApplicationWillTerminateNotification
                                           object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(backgroundCleanDisk)
                                             name:UIApplicationDidEnterBackgroundNotification
                                           object:nil];

听说下载失败后可以重新下载?

SD是有一个SDWebImageOptionsSDWebImageRetryFailed,但是这不意味着失败后会自己去重新尝试下载。
SDWebImageManager有一个数组failedURLs,用于存放所有下载图片失败的url,当加载图片的时候遇到上次失败过的url并且options没有设置为SDWebImageRetryFailed是直接不做处理的,反之才会根据失败的url重新加载。

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
    //....
    BOOL isFailedUrl = NO;
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }

    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        dispatch_main_sync_safe(^{
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
            completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
        });
        return operation;
    }
    //....
}

听说SD里面图片的唯一标示可以自定义?

SDWebImageManager提供了一个cacheKeyFilter供程序员设置,之后在要对图片操作前,会先根据url调用以下方法获取到唯一标示再进行后续操作。

- (NSString *)cacheKeyForURL:(NSURL *)url {
    if (!url) {
        return @"";
    }
    
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    } else {
        return [url absoluteString];
    }
}

官方给出了设置的方式

[[SDWebImageManager sharedManager] setCacheKeyFilter:^(NSURL *url) {
    url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
    return [url absoluteString];
}];

YYImage是如何做到逐行扫描的?

一般来说,图片下载,是通过将图片数据转换为NSData类型,然后顺序进行传递,接收到后再拼接的。YYImage的逐行扫描是因为图片本身有一个属性interlace,只有设置了这个属性才可能实现。(参考http://blog.csdn.net/code_for_free/article/details/51290067


如果本文让你有那么点觉得“I get”的感觉,请点个赞呗!写作很辛苦,路过请点赞!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容