读读SDWebImageDownloader

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


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

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

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

添加并执行一个下载任务

  • (id )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)