读读SDWebImageDownloader

作者: 萧子然 | 来源:发表于2016-03-23 11:56 被阅读1797次

    SD源码中下载只涉及到两个类,一个是SDWebImageDownloader,另外一个是SDWebImageDownloaderOperation。由文件命名不难看出,SD采用的是NSOperation异步下载的方式。下面就说说它的几个细节处理。


    • 如何实现一个下载manager?
    • 如何处理一个异步下载?
    • 一个下载任务具体实现?
    • 其他细节处理

    1. 如何实现一个下载manager?

    通常设计一个Manager就是对外提供一个统一的接口,Manager应该具备的功能主要有就是对被操作对象进行管理(增加,删除,修改)和处理操作(具体就是被操作对象的功能方法)并且维护任务状态。要实现一个Manager,常规做法就是维护一个对象容器,并对该容器进行相关操作。在此处,SDWebImageDownloader也是这样来实现的。其具体做法就是内部维护一个NSOperationqueue,来处理外部下载任务。同时,它也维护一个下载任务的容器,用来维护对应任务的一个状态。它提供了实以下两个方法,如下:

    添加并执行一个下载任务

    • (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
      options:(SDWebImageDownloaderOptions)options
      progress:(SDWebImageDownloaderProgressBlock)progressBlock
      completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

    设置是否挂起一个任务

    • (void)setSuspended:(BOOL)suspended;

    除了以上方法,SDWebImageDownloader提供了一些配置类操作,比如设置最大并发数,设置Http相关配置等。


    2. 如何处理一个异步下载?

    SDWebImageDownloader处理一个下载任务很简单,就只是把下载任务添加到并发队列,同时维护该下载任务的一个状态即可。

    1. 维护任务状态

    一个下载任务通常具有以下几个状态:1.下载开始,2.下载中,3.下载结束,4.下载取消。SDWebImageDownloader对外通过提供block回调来处理下载结束,下载中的一个状态。具体有以下几个block:

    typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
    
    typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData    *data, NSError *error, BOOL finished);
    
    typedef void(^SDWebImageNoParamsBlock)();
    

    SDWebImageDownloader在外部调用downloadImageWithURL时,通过被请求URL来创建一个字典用来维护该任务的一个状态处理block,在这里,SDWebImageDownloader对相同URL请求进行了过滤,即多次请求相同URL任务只会被处理一次。

    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }
    
        // Handle single download of simultaneous download request for the same URL
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        self.URLCallbacks[url] = callbacksForURL;
    
        if (first) {
            createCallback();
        }
    });
    

    block处理下载中状态

    SDWebImageDownloader *sself = wself;
    if (!sself) return;
    __block NSArray *callbacksForURL;
    dispatch_sync(sself.barrierQueue, ^{
        //获取当前状态维护队列
        callbacksForURL = [sself.URLCallbacks[url] copy];
        });
        
    
    for (NSDictionary *callbacks in callbacksForURL) {                                                              dispatch_async(dispatch_get_main_queue(), ^{
            //获取Url对应的下载处理block
            SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
            if (callback) callback(receivedSize, expectedSize);
        });
    }
    

    block处理下载完成状态

    SDWebImageDownloader *sself = wself;
    if (!sself) return;
    __block NSArray *callbacksForURL;
                                                                dispatch_barrier_sync(sself.barrierQueue, ^{
        callbacksForURL = [sself.URLCallbacks[url] copy];
        if (finished) {
            [sself.URLCallbacks removeObjectForKey:url];
        }
    });
    
    for (NSDictionary *callbacks in callbacksForURL) {
        //获取下载完成处理block
        SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
        if (callback) callback(image, data, error, finished);
    }
    

    block处理下载取消状态

    SDWebImageDownloader *sself = wself;
    if (!sself) return;
    dispatch_barrier_async(sself.barrierQueue, ^{
        [sself.URLCallbacks removeObjectForKey:url];
    });
    

    2. 创建任务并执行

    由于SDWebImageDownloader是通过NSOperation来处理并发任务,所以实现起来比较简单。创建一个NSOperation,并把它添加到Queue队列即可。在此处,SDWebImageDownloader创建了一个并发队列,默认的最大并发数6。
    SDWebImageDownloaderOperation是下载任务的具体实现类,它继承NSOperation,且该类通过Url请求来进行初始化。在处理队列中任务时,提供了两种处理方式,一种是FIFO,一种是LIFO。如果采用的是LIFO方式处理,则在把新任务加到队列的同时,需要更新一下最后入队列的任务,并且该任务依赖当前新加入的任务。具体实现如下:

    [wself.downloadQueue addOperation:operation];
    if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
        [wself.lastAddedOperation addDependency:operation];
        wself.lastAddedOperation = operation;
    }
    

    3. 一个下载任务具体实现

    SDWebImageDownloaderOperation是下载任务的具体实现类。该类维护了自生的一个状态,一个NSURLConnection,一个下载线程,还有具体的下载后数据。在NSOperation的start方法中。创建一个NSURLConnection并且开始这个connection,同时,开启一个后台线程用于下载数据。

    - (void)start {
        //初始化当前状态
        @synchronized (self) {
            if (self.isCancelled) {
                self.finished = YES;
                [self reset];
                return;
            }
    
            ...
            ...
            
            self.executing = YES;
            self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
            self.thread = [NSThread currentThread];
        }
        
        //执行connection
        [self.connection start];
    
        if (self.connection) {
            if (self.progressBlock) {
                self.progressBlock(0, NSURLResponseUnknownLength);
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
            });
    
            //开启线程
            if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
            }
            else {
                CFRunLoopRun();
            }
    
            if (!self.isFinished) {
                [self.connection cancel];
                [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
            }
        }
        else {
            if (self.completedBlock) {
                self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
            }
        }
    

    由于SDWebImageDownloaderOperation是通过NSURLConnection来下载图片。所以,具体的下载动作都由NSURLConnection的delegate来处理。

    收到http的response时的回调,在该回调里面,首先根据response返回码来判断请求是否成功。请求成功,则通过respone中文件大小来创建一个下载数据缓存区。请求失败,则主动取消改请求,并且停止后台下载线程,复位当前任务状态。

    - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
     //'304 Not Modified' is an exceptional one
        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;
            if (self.progressBlock) {
                self.progressBlock(0, expected);
            }
    
            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) {
                [self cancelInternal];
            } else {
                [self.connection cancel];
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
            });
    
            if (self.completedBlock) {
                self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES);
            }
            CFRunLoopStop(CFRunLoopGetCurrent());
            [self done];
        }
    }
    

    收到具体下载数据时候的回调,在该回调里面,首先对下载到的数据进行缓存,然后根据当前缓存到的数据,来获取到图片的长和宽,同时,根据请求下载的数据大小来做判断,如果符合,则生成下载的图片并回调给上层。

    - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
        [self.imageData appendData:data];
    
        if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
     
            // Get the total bytes downloaded
            const NSInteger totalSize = self.imageData.length;
    
            // Update the data source, we must pass ALL the data, not just the new bytes
            CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
    
            if (width + height == 0) {
                CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
                if (properties) {
                    NSInteger orientationValue = -1;
                    CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
                    if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
                    val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
                    if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
                    val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
                    if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
                    CFRelease(properties);
    
                    // When we draw to Core Graphics, we lose orientation information,
                    // which means the image below born of initWithCGIImage will be
                    // oriented incorrectly sometimes. (Unlike the image born of initWithData
                    // in connectionDidFinishLoading.) So save it here and pass it on later.
                    orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
                }
    
            }
    
            if (width + height > 0 && totalSize < self.expectedSize) {
                // Create the image
                CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
    
        #ifdef TARGET_OS_IPHONE
                // Workaround for iOS anamorphic image
                if (partialImageRef) {
                    const size_t partialHeight = CGImageGetHeight(partialImageRef);
                    CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
                    CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
                    CGColorSpaceRelease(colorSpace);
                    if (bmContext) {
                        CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
                        CGImageRelease(partialImageRef);
                        partialImageRef = CGBitmapContextCreateImage(bmContext);
                        CGContextRelease(bmContext);
                    }
                    else {
                        CGImageRelease(partialImageRef);
                        partialImageRef = nil;
                    }
                }
        #endif
    
        if (partialImageRef) {
            UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
            NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
            UIImage *scaledImage = [self scaledImageForKey:key image:image];
            if (self.shouldDecompressImages) {
                image = [UIImage decodedImageWithImage:scaledImage];
            }else {
                image = scaledImage;
            }
                CGImageRelease(partialImageRef);
                dispatch_main_sync_safe(^{
                        if (self.completedBlock) {
                            self.completedBlock(image, nil, nil, NO);
                        }
                    });
                }
            }
    
            CFRelease(imageSource);
        }
    
        if (self.progressBlock) {
            self.progressBlock(self.imageData.length, self.expectedSize);
        }
    }
    

    请求失败时的回调处理,终止后台下载线程,并复位当前请求。

    - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
        @synchronized(self) {
            CFRunLoopStop(CFRunLoopGetCurrent());
            self.thread = nil;
            self.connection = nil;
            dispatch_async(dispatch_get_main_queue(), ^{
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
            });
        }
    
        if (self.completedBlock) {
            self.completedBlock(nil, nil, error, YES);
        }
        self.completionBlock = nil;
        [self done];
    }
    

    请求加载完成时的回调处理,在该回调处理中,首先就是停止了后台下载线程,复位当前请求。然后判断该请求是否已经被缓存了,如果已经缓存,则取出缓存数据进行解码操作,生成相应图片并回调给上层

    - (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
        SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
        @synchronized(self) {
            CFRunLoopStop(CFRunLoopGetCurrent());
            self.thread = nil;
            self.connection = nil;
            dispatch_async(dispatch_get_main_queue(), ^{
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
                [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:self];
            });
        }
        
        if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
            responseFromCached = NO;
        }
        
        if (completionBlock) {
            if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) {
                completionBlock(nil, nil, nil, YES);
            } else if (self.imageData) {
                UIImage *image = [UIImage sd_imageWithData:self.imageData];
                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) {
                        image = [UIImage decodedImageWithImage:image];
                    }
                }
                if (CGSizeEqualToSize(image.size, CGSizeZero)) {
                    completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
                }
                else {
                    completionBlock(image, self.imageData, nil, YES);
                }
            } else {
                completionBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}], YES);
            }
        }
        self.completionBlock = nil;
        [self done];
    }
    

    除了以上几个比较重要的回调处理。SDWebImageDownloaderOperation还处理了http鉴权,加载等相关回调。

    4. 其他细节处理

    1. 通知上层应用

    针对具体的下载事件,需要及时通知上层,SDWebImageDownloaderOperation提供以下几个通知来做到上层同步。

    NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
    NSString *const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification";
    NSString *const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification";
    NSString *const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification";
    

    2. 后台线程的处理

    针对系统后台运行的处理

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

    针对后台下载线程的处理

    if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
            }
            else {
                CFRunLoopRun();
            }
    

    总结

    1. 掌握Manager与operatior的基本实现方式
    2. 加深NSOperation,NSURLConnection的应用

    相关文章

      网友评论

      本文标题:读读SDWebImageDownloader

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