SDWebImage源码解析

概述

SDWebImage是一个强大的图片下载框架,利用异步加载和内存+磁盘两级缓存处理,高效优雅的解决了图片下载的问题.

This library provides an async image downloader with cache support. For convenience, we added categories for UI elements like UIImageView, UIButton, MKAnnotationView.
引用官方文档的一句话:异步下载,支持缓存,并且利用分类的方式为UIImageView,UIButton,MKAnnotationView等控件添加了方便的下载方法,简言之就是,强大且好用.

一个常见的下载方法的调用是这样的:
[self.imageview sd_setImageWithURL:[NSURL URLWithString:@"http://www.pp3.cn/uploads/201609/2016092105.jpg"] placeholderImage:[UIImage imageNamed:@"dragon.jpg"] options:SDWebImageCacheMemoryOnly progress:^(NSInteger receivedSize, NSInteger expectedSize) {
        NSLog(@"%ld",expectedSize);
        NSLog(@"%ld",receivedSize);
    } completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
        NSLog(@"Finished!");
    }];

核心功能类:

UIImageView+WebCache**,UIButton+WebCache,MKAnnotationView+WebCache

作为直接面对用户调用的接口,框架通过给控件添加分类的形式方便的赋予了下载图片的功能.而这一切得益于后台几个类的相互配合;

SDWebImageManager

持有SDWebCache和SDWebImageDownloader对象,从而决定一个图片是从已有的缓存中取到,还是需要从网络下载;

SDWebImageDownloader

维护一个操作队列,并且管理下载过程中的进度和下载好的回调;

SDImageCache

负责为缓存开辟空间包括内存缓存和磁盘缓存,写入缓存,管理缓存,读出缓存,清理缓存等工作;

SDWebImageDownloaderOperation

这是一个NSOperation的子类,代表一个下载操作,以回调block或者代理的方式取到下载的图片数据或者错误信息.

按照软件工程的一般套路,我们剖析代码的思路也是:从整体到局部,从上层到下层,按功能线回溯,各行其是,分而治之

UI层

UI层通过给UIImageView,UIButton,MKAnnotationView添加分类,拓展了一个下载图片的方法,框架针对不同的情形,给出了多种方法可供调用,但是本质上都是调用了下面这个方法:

UIView+WebCacheOperation

考虑到很多UIView的子控件都需要持有当前的operation(s),作者添加了一个UIView的分类,用来存储加载视图上面的operation(s),同时添加了一系列方法来维护这些operation(s).

//设置图片下载操作
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key;
//取消图片下载操作
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key;
///移除图片操作
- (void)sd_removeImageLoadOperationWithKey:(NSString *)key;

但是为什么要这么做呢?举例来说,tableViewCell上面有很多UIImageView,快速滑动的时候,其实很多imageView快速出来,又快速消失了,这个时候把消失了的视图上面的下载操作取消掉,是更好的选择.

UIImageView+WebCache

这个分类里面,提供了很多便利化的方法可供调用,但是最终都会调到下面这个方法的实现,我在这个方法里面添加了注释:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
//url: 图片对应的URL
//placeholder: 预显示的占位图片,从项目的Bundle里面加载;
//option: SDWebImageOptions的枚举,定义了一系列用户的定制项;
//progressBlock: SDWebImageDownloaderProgressBlock下载进度的回调block,用户可以在这里取到要下载的文件大小,和已下载的文件大小,用来跟踪下载进度或者定制进度条;
//completedBlock: 下载完成的回调block,包含image,error,url,以及完成与否的bool值.

    [self sd_cancelCurrentImageLoad];//取消对应当前控件对应的下载任务
    
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //将当前UIImageView对象与其URL进行关联:  UIImageView <--> Url
    if (!(options & SDWebImageDelayPlaceholder)) {//
        dispatch_main_async_safe(^{
            self.image = placeholder;//添加占位图片
        });
    }
    
    if (url) {
        // check if activityView is enabled or not
        // 添加下载指示器
        if ([self showActivityIndicatorView]) {
            [self addActivityIndicator];
        }
        
        //调用SDWebImageManager单例的下载方法,返回一个遵循了SDWebImageOperation协议的operation对象.
        __weak __typeof(self)wself = self;
        id  operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            [wself removeActivityIndicator];//完成移除指示器
            if (!wself) return;
            dispatch_main_sync_safe(^{
                if (!wself) return;
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else if (image) {
                    wself.image = image;
                    [wself setNeedsLayout];
                } else {
                    if ((options & SDWebImageDelayPlaceholder)) {
                        wself.image = placeholder;
                        [wself setNeedsLayout];
                    }
                }
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];//添加当前控件的operation
    } else {//当url为空的时候
        dispatch_main_async_safe(^{
            [self removeActivityIndicator];//移除指示器
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);//调用完成的block抛出异常信息
            }
        });
    }
}

其他的控件上的下载操作和这个类似,这里就不一一列举了,方法调到这里就会离开UI层而来到控制层.

Tips:
1.OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
这是利用Runtime来给分类添加属性的方法,object是要关联属性的对象,key是一个常量字符,value是要关联的属性,policy是关联策略.

2.dispatch_main_async_safe(block)
这是自定义的宏,用来保证block只在主线程异步执行.

控制层

控制层利用一个SDWebImageManager来协调图片是从网路下载还是从缓存中获取,并且决定是否刷新缓存,SDWebImageManager持有SDImageCache和SDWebImageDownloader对象.

SDWebImageManager

职能:

  1. 发出下载指令,并取得下载的操作
  2. 指定Url转换成缓存用的唯一key
  3. 发出缓存指令,给定key
  4. 检查缓存是否存在,包括在缓存和磁盘中
  5. 取消所有的下载操作

实现:

定义SDWebImageOptions枚举

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    SDWebImageRetryFailed = 1 << 0,//1 是否重试
    SDWebImageLowPriority = 1 << 1,//2 当前若有UI操作的时候,延迟下载
    SDWebImageCacheMemoryOnly = 1 << 2,//4 只允许内存缓存\不允许磁盘缓存
    SDWebImageProgressiveDownload = 1 << 3,//8 采用渐进式加载\默认情况下是图片下载完成之后才渲染
    SDWebImageRefreshCached = 1 << 4,//16 刷新缓存
    SDWebImageContinueInBackground = 1 << 5,//32 开启后台下载功能
    SDWebImageHandleCookies = 1 << 6,//64 为请求保存Cookies
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,//128 允许无效的SSL证书
    SDWebImageHighPriority = 1 << 8,//256 把下载任务移到任务队列的front位置,保证优先级最高
    SDWebImageDelayPlaceholder = 1 << 9,//512 延迟加载占位图片
    SDWebImageTransformAnimatedImage = 1 << 10,//1024 使用动画图片 
    SDWebImageAvoidAutoSetImage = 1 << 11//2048 避免自动设置图片
};

Manager通过SDWebImageDownloader对象下达加载图片的指令,都在下面这个方法里面:

- (id )downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock

1.判断url是否有效,无效则把url指定为nil,如果是以前就失败了的url并且options为SDWebImageRetryFailed,则调用completedBlock给出错误提示,方法返回当前操作,否则进行步骤2;
2.往runningOperations增加operation,并把url转换成标识图片的key;
3.调用imageCache的- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock方法,从内存和磁盘里查询缓存,如果缓存存在调用completedBlock,返回查找到的图片,否则进行步骤4;
4.如果没有命中缓存,或者指定需要刷新缓存,则开始启动imageDownloader的下载程序- (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock,把返回的operation添加到下载队列中,这里并没有提供磨人的progressBlock的实现,只是在completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished)里面处理下载结束的状态,错误信息,返回下载好的图片并把图片在存到缓存中.

- (id )loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock {
    //completedBlock为空时候的异常处理
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
    
    //对url的纠错处理,如果传入的是字符串类型,则强制转换成NSURL
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }
    
    //如果是其他不能强制转换成NSURL的类型,或者是NSNull,则直接置为nil,后面碰到nil就会走处理nil的部分
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;
    
    //failedURLs用来保存失败了的urls,加互斥锁用保证多线程安全
    BOOL isFailedUrl = NO;
    if (url) {
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    }
    
    //如果给定的url不满足条件,开始往失败的分支里面走,其实就是调用completedBlock给定空的image
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
        return operation;
    }
    
    //如果能走到这里来,说明操作本次操作可以执行,就添加到正在执行的operations
    //同样加互斥锁保证线程安全
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    
    //通过url转换成key,默认情况是直接返回url.absoluteString,如果用户有给定对url的筛选规则,则考虑自定义的规则
    NSString *key = [self cacheKeyForURL:url];
    
    //调用SDImageCache的查询缓存的方法,通过key来查找缓存
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        //如果操作中途被取消,则用线程安全的方式取消正在进行中的operation,然后方法返回
        if (operation.isCancelled) {
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }
        
        //如果用户要刷新缓存 && 没有自己实现下载图片的方法,那就要走框架给定的下载方法
        if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            if (cachedImage && options & SDWebImageRefreshCached) {
                
                //如果命中缓存图片,但是用户强制刷新缓存,那就先提供缓存图片,同时再去服务端下载一边,当图片的内容改变了,但是url没有变的时候适用
                [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }
            
            //如果没有图片或者强制刷新,则开始从服务器加载图片
            SDWebImageDownloaderOptions downloaderOptions = 0;
            //...
            
            //开启创建下载任务
            SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                    //如果操作被取消,那就什么都不做
                } else if (error) {
                    //下载出现错误的处理
                    [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
                    //...
                }
                else {
                    if ((options & SDWebImageRetryFailed)) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            //对动画图片(GIF)的处理
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                //调用SDWebImageCache的存储方法,开始往内存或者磁盘里面缓存图片内容,key是上文中给定的url,在SDWebImageCache类的内部还会对key做进一步的处理
                                // pass nil if the image was transformed, so we can recalculate the data from the image
                                [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            //调用completetionBlock返回结果
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {
                        if (downloadedImage && finished) {
                            //调用SDWebImageCache的存储方法,开始往内存或者磁盘里面缓存图片内容,key是上文中给定的url,在SDWebImageCache类的内部还会对key做进一步的处理
                            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                        }
                        //调用completetionBlock返回结果
                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }

                if (finished) {
                    //安全移除完成了的操作
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];
            operation.cancelBlock = ^{
                [self.imageDownloader cancel:subOperationToken];
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                [self safelyRemoveOperationFromRunning:strongOperation];
            };
        } else if (cachedImage) {
            //命中缓存并且不刷新缓存则,直接回调completetionBlock,返回缓存
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            //安全移除完成了的操作
            [self safelyRemoveOperationFromRunning:operation];
        } else {
            //既没有缓存,也不允许下载,则返回错误
            // Image not in cache and download disallowed by delegate
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }
    }];
    
    //返回当前操作对象
    return operation;
}

Tips:
1.类型纠错
有时候我们误把NSString类型当做NSURL,或者传递的是非URL得类型,框架会帮我们做类型纠错,把字符串类型转换成NSURL,如果是非URL类型,就转换成nil,以免造成后面的误操作.

2.线程加锁

BOOL isFailedUrl = NO;
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];//
    }

框架定义了一个NSMutableSet类型的failedURLs,用来存储那些失败了的urls,但是考虑到多个线程要操作这个结构,所以用@synchronized加了个互斥锁,以免造成线程安全问题.

//添加当前下载操作
@synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];//
    }
//移除当前下载操作
@synchronized (self.runningOperations) {
                        if (strongOperation) {
                            [self.runningOperations removeObject:strongOperation];
                        }
                    }
//移除所有当前下载操作
@synchronized (self.runningOperations) {
        NSArray *copiedOperations = [self.runningOperations copy];
        [copiedOperations makeObjectsPerformSelector:@selector(cancel)];
        [self.runningOperations removeObjectsInArray:copiedOperations];
    }
//判断有没有正在进行的下载操作
- (BOOL)isRunning {
    BOOL isRunning = NO;
    @synchronized(self.runningOperations) {
        isRunning = (self.runningOperations.count > 0);
    }
    return isRunning;
}

runningOperations是一个可变数组,可以用以对当前的operations进行操作,同样需要保证在多线程环境下的安全性.

SDWebImageDownloader

这个类其实已经很接近下载操作了,但是还是一个中间层.

职能:
1.配置NSURLSession的默认配置(包括请求头信息,超时时间,urlCredential等),生成NSURLSession对象;
2.维护一个下载队列downloadQueue,可以往队列里面添加任务,删除任务,或者调配任务的执行顺序;
3.根据session和request创建operation对象,并加到操作队列里面,指定下载任务和操作的对应关系;
4.把task的返回数据的代理方法,指向对应的operation的代理方法里面.

实现:

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;
    
    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        __strong __typeof (wself) sself = wself;
        
        //设置请求的时间间隔
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        //设置缓存策略,创建urlRequest
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        
        //设置请求头信息
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        } else {
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        
        //创建下载的操作对象operation,关联了request和session
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        
        //是否解压缩图片
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        
        //设置请求的身份验证信息
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        //设置队列优先级
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        
        //将操作加入到队列中
        [sself.downloadQueue addOperation:operation];
        
        //设置队列中任务的执行顺序,默认是FIFO
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }
        
        //返回operation
        return operation;
    }];
}

另外在请求接收数据的代理方法里面,会根据task对应的dataOperation请用指定的代理方法,这样就实现了downloader这个类只是起协调作用,真正接受处理数据还是在operation里面,下面会介绍:

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {

    // Identify the operation that runs this task and pass it the delegate method
    SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];

    [dataOperation URLSession:session dataTask:dataTask didReceiveData:data];
}

SDWebImageDownloaderOperation

正在处理下载任务和接受数据的地方.

职能:
1.由于继承自NSOperation所以,本类有一系列对operation的操作,例如,start,cancel,done,reset,finished;
2.持有request和session,如果为空则在start的方法里面创建请求和发起dataTask;
3.在代理方法里面接受和处理数据.

实现:

- (void)start {
    //取消和重置当前操作
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }
        
        //TARGET_OS_IOS || TARGET_OS_TV 如果是iOS平台或者TV平台,则会在应用切到后台的时候开启后台下载任务
#if SD_UIKIT
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;
                
                if (sself) {
                    [sself cancel];
                    
                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif
        
        NSURLSession *session = self.unownedSession;
        if (!self.unownedSession) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            //为下载任务创建session,当delegateQueue为nil的时候,会默认在一个串行队列中执行代理方法
            self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            session = self.ownedSession;
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    
    [self.dataTask resume];
    
    if (self.dataTask) {
        //处理任务进度的回调
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
    } else {
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
    }
    
#if SD_UIKIT
//结束后台任务
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
        UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
        [app endBackgroundTask:self.backgroundTaskId];
        self.backgroundTaskId = UIBackgroundTaskInvalid;
    }
#endif
}

start方法是发起下载任务的地方,并且考虑到了后台任务的情况,对于iOS平台和TV平台,当应用切换到后台的时候,会以一个指定的id继续下载,在任务开始的收会处理任务进度的回调和发送一个SDWebImageDownloadStartNotification的通知.

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    
    //'304 Not Modified' 表明服务端的内容没有做修改,这种情形会单独考虑
    if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode < 400 && ((NSHTTPURLResponse *)response).statusCode != 304)) {
        NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
        self.expectedSize = expected;
        //获取到将要接受的数据的二进制形式的长度,调用progressBlock处理下载进度
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, expected, self.request.URL);
        }
        
        self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
        self.response = response;
        //在主线程发出一个SDWebImageDownloadReceiveResponseNotification
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
        });
    } else {
        NSUInteger code = ((NSHTTPURLResponse *)response).statusCode;
        //如果响应状态码为304服务端的内容没有做修改,这时候不用去接受图片数据,只需要取消当前操作并返回缓存中的图片就好了
        //这样能节省用户流量,提高效率
        if (code == 304) {
            [self cancelInternal];
        } else {
            [self.dataTask cancel];
        }
        //在主线程发出SDWebImageDownloadStopNotification的通知
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
        });
        
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:((NSHTTPURLResponse *)response).statusCode userInfo:nil]];
        
        [self done];
    }
    
    if (completionHandler) {
        completionHandler(NSURLSessionResponseAllow);
    }
}

由于框架用的直接是系统的UrlSession加载数据,所以在接收到response的时候需要声明接收数据的NSData对象,对于304的隔离处理能有效的帮助用户节省流量和提高图片显示效率.

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data

这里主要是将接收到的图片二进制数据拼接到imageData里面去,并处理下载的进度情况,值得注意的是如果用户指定了以边加载边显示的方式渲染图片的话,这里做了很多画图的工作,这里就不详述图片的绘制过程,有兴趣的同学请跳

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error

下载完成之后,对图片数据进行尺寸的调整和压缩,并且调用回调block,至此从服务端下载图片的流程就能走通了.

SDImageCache

这里真正执行图片缓存的地方,涉及到缓存的增,删,查找的诸多操作,还有很多异步的文件操作..

耳熟能详的枚举:

typedef NS_ENUM(NSInteger, SDImageCacheType) {
    SDImageCacheTypeNone,//不缓存
    SDImageCacheTypeDisk,//磁盘缓存
    SDImageCacheTypeMemory//内存缓存
};

根据manager给定的key(默认是图片的url)生成存储文件的文件名,这里就是采用MD5摘要算法,生成唯一文件名.

- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
    const char *str = key.UTF8String;
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]];

    return filename;
}

在高版本的SD中,作者把判断图片文件类型的方法单独到了一个NSData+ImageContentType的类里面,用到的判断依据是:拿到一张图片十六进制数据的第一个字节进行判断,然后再根据不同的结果返回图片的真实类型。只要是同一种类型的图片,它十六进制数据的第一个字节都是相同的,JPEG图片十六进制数据的第一个字节是0xFF,PNG图片十六进制数据的第一个字节是0x89,GIF图片十六进制数据的第一个字节是0x47.

核心方法:查询缓存

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {   
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }       
    //查看内存缓存
   UIImage *image = [self imageFromMemoryCacheForKey:key];    
    if (image) {    
        NSData *diskData = nil;        
        if ([image isGIF]) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }        
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }        
        return nil;
    }
    //查看磁盘缓存
    NSOperation *operation = [NSOperation new];    
    dispatch_async(self.ioQueue, ^{        
        if (operation.isCancelled) {
            // 在用之前就判断operation是否被取消了,作者考虑的非常严谨
            return;
        }
        @autoreleasepool {            
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];            
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);                
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });
    return operation;
}

结语

致此已经分析了SDWebImage框架的核心内容,可见框架的博大,还没有涉及到的以后再补充,源代码是程序员智慧的结晶,向大神致敬.

你可能感兴趣的:(SDWebImage源码解析)