机制第三方库精讲程序员

SDWebImage下载模块解析

2017-02-02  本文已影响1005人  要上班的斌哥

在iOS的图片加载框架中,SDWebImage可谓是占据大半壁江山。它支持从网络中下载且缓存图片,并设置图片到对应的UIImageView控件或者UIButton控件。在项目中使用SDWebImage来管理图片加载相关操作可以极大地提高开发效率,让我们更加专注于业务逻辑实现。关于 SDWebImage的源码解读可以参考我的 另一篇博客iOS图片加载框架-SDWebImage解读.本篇文章的重点主要放在SDWebImage框架的图片下载模块解读。

Paste_Image.png

从 SDWebImage的Architecture图中可以看到,SDWebImage框架的图片下载模块主要是以SDWebImageDownloader为主。那么我们就可以沿着SDWebImageDownloader这个切入点逐步去探索整个图片下载流程和细节。

SDWebImageDownloader 是一个下载管理器

SDWebImageDownloader类图

从SDWebImageDownloader的类图信息可以清晰的看出来,SDWebImageDownloader的主要任务是下载方面管理,包括下载队列的先后顺序,最大下载任务数量控制,下载队列中的任务创建,取消,暂停等任务管理,以及其他 HTTPS 和 HTTP Header的设置。

//传入图片的URL,图片下载过程的Block回调,图片完成的Block回调
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;
    // 传入对应的参数,addProgressCallback: completedBlock: forURL: createCallback: 方法
    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;
        }

        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        // Header的设置
        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;
        }
        //创建 SDWebImageDownloaderOperation,这个是下载任务的执行者。并设置对应的参数
        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];
        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;
        }

        return operation;
    }];
}

该方法主要操作在于创建一个SDWebImageDownloaderOperation 对象,然后对该进行属性设置,再将该对象加入名字为downloadQueue 的队列。

- (void)cancel:(nullable SDWebImageDownloadToken *)token {
    dispatch_barrier_async(self.barrierQueue, ^{
        //根据token取出对应的SDWebImageDownloaderOperation对象然后调用该对象的cancel: 方法
        SDWebImageDownloaderOperation *operation = self.URLOperations[token.url];
        BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
        if (canceled) {
            [self.URLOperations removeObjectForKey:token.url];
        }
    });
}

有创建SDWebImageDownloaderOperation 对象,自然也有取消方法。该方法从保存SDWebImageDownloaderOperation对象的字典中取出token对应的SDWebImageDownloaderOperation对象,然后调用该对象的取消方法。

- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration {
    if ((self = [super init])) {
        // ...
        sessionConfiguration.timeoutIntervalForRequest = _downloadTimeout;

        /**
         *  Create the session for this task
         *  We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
         *  method calls and completion handler calls.
         */
        self.session = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                                     delegate:self
                                                delegateQueue:nil];
    }
    return self;
}

在SDWebImageDownloader的初始化方法中有创建了一个NSURLSession,这个对象是用来执行图片下载的网络请求的,它的代理对象是SDWebImageDownloader。所以我们在SDWebImageDownloader类里面找到对应的NSURLSession的代理方法,那么就可以看到下载操作的一些细节。

#pragma mark NSURLSessionDataDelegate

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {

    // Identify the operation that runs this task and pass it the delegate method
    // 取得 SDWebImageDownloaderOperation 对象将URLSession的回调转发给它
    SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];

    [dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}

- (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];
    // 取得 SDWebImageDownloaderOperation 对象将URLSession的回调转发给它
    [dataOperation URLSession:session dataTask:dataTask didReceiveData:data];
}

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {

    // Identify the operation that runs this task and pass it the delegate method
    SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];
    // 取得 SDWebImageDownloaderOperation 对象将URLSession的回调转发给它
    [dataOperation URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
}

#pragma mark NSURLSessionTaskDelegate

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    // Identify the operation that runs this task and pass it the delegate method
    SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:task];
    // 取得 SDWebImageDownloaderOperation 对象将URLSession的回调转发给它
    [dataOperation URLSession:session task:task didCompleteWithError:error];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
    
    completionHandler(request);
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {

    // Identify the operation that runs this task and pass it the delegate method
    SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:task];
    // 取得 SDWebImageDownloaderOperation 对象将URLSession的回调转发给它
    [dataOperation URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler];
}

从上面的NSURLSession的各个回调方法中的操作可以看到,所有的方法都是被转发到SDWebImageDownloaderOperation对象,该对象隐藏在SDWebImageDownloader背后默默的干活,这个也符合我们封装的原则。

SDWebImageDownloaderOperation 是下载任务处理者

SDWebImageDownloaderOperation.png

从这个架构图中可以看出SDWebImageDownloaderOperation 继承于 NSOperation ,实现了SDWebImageOperation和SDWebImageDownloaderOperationInterface协议。当然最重要的还是要看NSURLSession的回调方法处理。

//收到响应
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    
    //'304 Not Modified' is an exceptional one
    // (没有statusCode) 或者 (statusCode小于400 并且 statusCode 不等于304)
    // 若是请求响应成功,statusCode是200,那么会进入这个代码分支
    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;
        //下载过程回调,收到数据量为0
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, expected, self.request.URL);
        }
        //根据期望收到的数据量创建NSMutableData,该NSMutableData用户保存收到的数据量
        self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
        //保存响应对象
        self.response = response;
        //发送网络请求收到响应的通知
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
        });
    }
    else {
        NSUInteger code = ((NSHTTPURLResponse *)response).statusCode;
        //This is the case when server returns '304 Not Modified'. It means that remote image is not changed.
        //In case of 304 we need just cancel the operation and return cached image from the cache.
        if (code == 304) {
            //code 304 取消下载 直接返回缓存中的内容
            [self cancelInternal];
        } else {
            //数据请求任务取消
            [self.dataTask cancel];
        }
        
        //发送停止下载的通知
        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];
    }
    // 调用completionHandler回调
    if (completionHandler) {
        completionHandler(NSURLSessionResponseAllow);
    }
}

收到网络请求的响应之后判断statusCode,若是正常的statusCode,那么就根据数据量创建对应大小的NSMutableData,发送对应的通知。若是出现异常,那么就做好对应的错误回调和发送对应的错误通知。

// 收到请求数据
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    //组装请求数据
    [self.imageData appendData:data];

    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
    //渐进式加载图片处理
    }
    //下载过程回调
    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
    }
}

不断收到响应数据,不断地调用下载过程的回调Block.

//数据请求任务完成
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    @synchronized(self) {
        self.dataTask = nil;
        dispatch_async(dispatch_get_main_queue(), ^{
            // 发送停止下载通知
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
            if (!error) {
                //发送完成下载通知
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
            }
        });
    }
    
    if (error) {
        //错误回调
        [self callCompletionBlocksWithError:error];
    } else {
        if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
            /**
             *  See #1608 and #1623 - apparently, there is a race condition on `NSURLCache` that causes a crash
             *  Limited the calls to `cachedResponseForRequest:` only for cases where we should ignore the cached response
             *    and images for which responseFromCached is YES (only the ones that cannot be cached).
             *  Note: responseFromCached is set to NO inside `willCacheResponse:`. This method doesn't get called for large images or images behind authentication
             */
            if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached && [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]) {
                // hack
                [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
            } else if (self.imageData) {
                //处理图片方向和图片格式
                UIImage *image = [UIImage sd_imageWithData:self.imageData];
                // 获取缓存key
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                // 处理图片的缩放倍数
                image = [self scaledImageForKey:key image:image];
                
                // Do not force decoding animated GIFs
                // 动图不要解压
                if (!image.images) {
                    if (self.shouldDecompressImages) {
                        if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
#if SD_UIKIT || SD_WATCH
                            image = [UIImage decodedAndScaledDownImageWithImage:image];
                            [self.imageData setData:UIImagePNGRepresentation(image)];
#endif
                        } else {
                            image = [UIImage decodedImageWithImage:image];
                        }
                    }
                }
                // 回调处理
                if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                    [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]];
                } else {
                    [self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES];
                }
            } else {
                [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
            }
        }
    }
    // 完成
    [self done];
}

图片数据下载完成之后,最重要的一件事就是将图片数据转成UIImage对象。然后通过 - (void)callCompletionBlocksWithImage:imageData:error:finished: 方法 将该UIImage对象和图片数据的data 传给SDWebImageDownloader。

图片数据下载完成后,还需要生成UIImage对象

+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data {
    if (!data) {
        return nil;
    }
    
    UIImage *image;
    //识别图片格式
    SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:data];
    if (imageFormat == SDImageFormatGIF) {
    // gif图片处理,得到静态图片
        image = [UIImage sd_animatedGIFWithData:data];
    }
//是否开启了 WEBP 格式支持
#ifdef SD_WEBP
    // webp图片处理
    else if (imageFormat == SDImageFormatWebP)
    {
        // 得到webp格式图片
        image = [UIImage sd_imageWithWebPData:data];
    }
#endif
    // 静态图片处理
    else {
        image = [[UIImage alloc] initWithData:data];
#if SD_UIKIT || SD_WATCH
        //图片方向处理
        UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];
        // 图片的方向默认是UIImageOrientationUp,若是本来图片的方向就是UIImageOrientationUp那么就不需要重新处理图片的方向
        if (orientation != UIImageOrientationUp) {
            image = [UIImage imageWithCGImage:image.CGImage
                                        scale:image.scale
                                  orientation:orientation];
        }
#endif
    }


    return image;
}

识别出对应的图片格式,对GIF,WEBP格式做正确的处理。识别出图片的方向,然后生成方向正确的图片。值得注意的是在SDWebImage 4.0 之后 GIF 图片使用 FLAnimatedImageView 来支持。

// 返回正确的缩放倍数图片
inline UIImage *SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image) {
    if (!image) {
        return nil;
    }
    
#if SD_MAC
    return image;
#elif SD_UIKIT || SD_WATCH
    // 动图处理
    if ((image.images).count > 0) {
        NSMutableArray<UIImage *> *scaledImages = [NSMutableArray array];

        for (UIImage *tempImage in image.images) {
            //递归处理
            [scaledImages addObject:SDScaledImageForKey(key, tempImage)];
        }
       // 处理动图播放
        return [UIImage animatedImageWithImages:scaledImages duration:image.duration];
    }
    else {
#if SD_WATCH
        if ([[WKInterfaceDevice currentDevice] respondsToSelector:@selector(screenScale)]) {
#elif SD_UIKIT
        if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
#endif
            //判断图片的缩放倍数,默认为 1 
            CGFloat scale = 1;
            // 判断key的命名规则是否有包含缩放倍数
            if (key.length >= 8) {
                NSRange range = [key rangeOfString:@"@2x."];
                if (range.location != NSNotFound) {
                    scale = 2.0;
                }
                
                range = [key rangeOfString:@"@3x."];
                if (range.location != NSNotFound) {
                    scale = 3.0;
                }
            }
            //创建UIImage对象
            UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
            image = scaledImage;
        }
        return image;
    }
#endif
}

除了图片格式和图片方向。SDWebImage还可以对@2x,@3x后缀的图片做正确的缩放处理。


+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
    //图片是否可以解压,若是不适合解压则直接返回原图片
    if (![UIImage shouldDecodeImage:image]) {
        return image;
    }
    
    
    // autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
    // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
    @autoreleasepool{
        
        CGImageRef imageRef = image.CGImage;
        //CGColorSpaceRef : A profile that specifies how to interpret a color value for display.
        CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];
        //宽度
        size_t width = CGImageGetWidth(imageRef);
        //高度
        size_t height = CGImageGetHeight(imageRef);
        //kBytesPerPixel 每个像素有4个字节
        //图片每行的数据量
        size_t bytesPerRow = kBytesPerPixel * width;

        // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
        // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
        // to create bitmap graphics contexts without alpha info.
        // kBitsPerComponent(一个颜色由几个bit表示) :  the number of bits allocated for a single color component of a bitmap image.
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     kBitsPerComponent,
                                                     bytesPerRow,
                                                     colorspaceRef,
                                                     kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
        if (context == NULL) {
            return image;
        }
        
        // Draw the image into the context and retrieve the new bitmap image without alpha
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        // 在这个图片处理过程中丢失了图片的透明度信息
        UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
                                                         scale:image.scale
                                                   orientation:image.imageOrientation];
        
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        // 没有透明度信息的图片
        return imageWithoutAlpha;
    }
}

在上述方法的处理过程中,会丢失图片的Alpha信息,但是SDWebImage在处理之前有保存了这个Alpha信息,处理OK,之后生成的UIImage对象是包含正确的Alpha信息。SDWebImage把图片用 CGContextDrawImage() 绘制到画布上,然后把画布的数据取出来当作图片。这个是可以避免系统的UIImage缓存,这也是常见的网络图片库的做法。

+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
    //是否需要解码图片
    if (![UIImage shouldDecodeImage:image]) {
        return image;
    }
    //是否需要缩小图片
    if (![UIImage shouldScaleDownImage:image]) {
        //不需要缩小的图片则进行图片解码
        return [UIImage decodedImageWithImage:image];
    }
    
     //SDWebImage中的图片缩小的代码来自Apple的官方Demo,里面有比较详细的注释 https://developer.apple.com/library/content/samplecode/LargeImageDownsizing/Introduction/Intro.html
}

可以通过设置下载选项,让SDWebImage对超过特定大小的图片进行缩小,然后再存入缓存。该图片缩小的代码来自Apple 的官方 Demo 不再做详细注解。

部分架构图

SDWebImageDownloader拿到下载后的图片data和UIImage对象之后,将这些内容返回给SDWebImageManager,SDWebImageManager将UIImage对象存入SDImageCache缓存中。到这里SDWebImage的下载模块的任务就全部完成了。

个人能力有限,若是大家有发现文章中的不足或者错误之处,恳请在评论中指出,我会进行修正。

参考资料

SDWebImage 项目 https://github.com/rs/SDWebImage
iOS 处理图片的一些小 Tip http://blog.ibireme.com/2015/11/02/ios_image_tips/
Apple 官方 Demo(LargeImageDownsizing) https://developer.apple.com/library/content/samplecode/LargeImageDownsizing/Introduction/Intro.html
}

上一篇下一篇

猜你喜欢

热点阅读