一问一答看SDWebImage源码

怎样进行图片下载?

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

//SDWebImageDownloader.m
- (id )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 

@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 )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  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 )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 )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”的感觉,请点个赞呗!写作很辛苦,路过请点赞!

你可能感兴趣的:(一问一答看SDWebImage源码)