iOS----------SDWebimage源码解析(5)

本文详细介绍了 iOS 图片下载组件 SDWebImage 中的 SDWebImageDownloaderOperation 类。通过分析源码,深入探讨了图片下载流程、代理方法及缓存机制等内容。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本篇我们来介绍下载图片的类,创建SDWebImageDownloaderOperation对象,它继承于NSOperation,遵守SDWebImageOperation协议,下面我们来看看SDWebImageDownloaderOperation类的源码。使用SDWebImageDownloaderOperation来封装任务。
1、SDWebImageDownloaderOperation.h文件
一些属性大家从源码中就可以看到,有一个主要的方法

- (id)initWithRequest:(NSURLRequest *)request
              options:(SDWebImageDownloaderOptions)options
             progress:(SDWebImageDownloaderProgressBlock)progressBlock
            completed:(SDWebImageDownloaderCompletedBlock)completedBlock
            cancelled:(SDWebImageNoParamsBlock)cancelBlock;

2、SDWebImageDownloaderOperation.m文件
看了SDWebImageDownloaderOperation.m文件才发现真正的核心代码并不是初始化方法,在init方法中只是对传入的参数的赋值和获取

- (id)initWithRequest:(NSURLRequest *)request
              options:(SDWebImageDownloaderOptions)options
             progress:(SDWebImageDownloaderProgressBlock)progressBlock
            completed:(SDWebImageDownloaderCompletedBlock)completedBlock
            cancelled:(SDWebImageNoParamsBlock)cancelBlock {
    if ((self = [super init])) {
        _request = request;
        _shouldDecompressImages = YES;
        _shouldUseCredentialStorage = YES;
        _options = options;
        _progressBlock = [progressBlock copy];
        _completedBlock = [completedBlock copy];
        _cancelBlock = [cancelBlock copy];
        _executing = NO;
        _finished = NO;
        _expectedSize = 0;
        responseFromCached = YES; // Initially wrong until `connection:willCacheResponse:` is called or not called
    }
    return self;
}

(2)、重写operation的start方法


- (void)start {
    //线程同步加锁
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
        //获取系统的application
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {

            __weak __typeof__ (self) wself = self;
            //获取单例app
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            //获取这个后台线程的标示UIBackgroundTaskIdentifier
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;

                //后台下载时间到了,就会调用这个block,如果任务仍在下载就进行取消,调用endBackgroundTask这个方法通知系统该backgroundTaskId停止,并把backgroundTaskId的状态改为无效
                if (sself) {
                    [sself cancel];

                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif

        self.executing = YES;
        //创造connection
        self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
        self.thread = [NSThread currentThread];
    }

    //开始
    [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) {
            // Make sure to run the runloop in our background thread so it can process downloaded data
            // Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
            //       not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466)
            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);
        }
    }

#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
    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
}

上面方法中创建了一个NSURLConnection对象,苹果已经将NSURLConnection用NSURLSession代替,但在SDWebimage中还没有替换,可能下一个版本会替换吧。创建NSURLConnection对象后开始下载图片。

开始下载图片后,会调用NSURLConnection代理中的方法

#pragma mark NSURLConnection (delegate)
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response 
{

    //'304 Not Modified' is an exceptional one
//    这里有对response的处理
    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);
        }

        //创建一个收集数据的空间   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.

        //如果code是304表示新加载的数据与原来的相同,那么就不需要发送请求,可以从缓存中取到
        //场景:如果从后台请求的图片地址没有变化,图片发生了变化,在请求这张图片的时候需要设置这张图片在加载的时候刷新缓存,如果图片发生变化那么就会去调用请求,如果没有变化那么那么这里会拦截,关闭网络请求
        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);
        }

        //停止runloop
        CFRunLoopStop(CFRunLoopGetCurrent());
        [self done];
    }
}

第二个代理方法,获取到Data


- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {

    //拼接数据
    [self.imageData appendData:data];

    //获取下载进度
    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
        // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
        // Thanks to the author @Nyx0uf

        // 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

        //根据data获取CGImageSourceRef
        CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);

        if (width + height == 0) {
            //获取imageSource中的属性
            CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
            if (properties) {

                //初始方向
                NSInteger orientationValue = -1;

                //获取高度
                CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
                //将高度copy到height中
                if (val) CFNumberGetValue(val, kCFNumberLongType, &height);


                //获取宽度
                val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
                //将宽度copy到width中
                if (val) CFNumberGetValue(val, kCFNumberLongType, &width);

                //获取方向
                val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
                //将方向copy到orientationValue中
                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)];
            }

        }
        //宽高都不为0

        if (width + height > 0 && totalSize < self.expectedSize) {
            // Create the image
            //创建image
            CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);

#ifdef TARGET_OS_IPHONE

            // 解决iOS平台图片失真问题
            // 因为如果下载的图片是非png格式,图片会出现失真
            // 为了解决这个问题,先将图片在bitmap的context下渲染
            // 然后在传回partialImageRef
            // Workaround for iOS anamorphic image
            if (partialImageRef) {
                const size_t partialHeight = CGImageGetHeight(partialImageRef);
                //创建rgb空间
                CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
                //获取上下文 bmContext
                CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
                //释放rgb空间
                CGColorSpaceRelease(colorSpace);


                if (bmContext) {
                    //绘制图片到context中  这里的高度为partialHeight  因为height只在宽高都等于0的时候才进行的赋值,所以以后的情况下partialHeight都等于0,所以要使用当前数据(imageData)转化的图片的高度,partialImageRef为要绘制的image
                    CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
                    //释放partialImageRef
                    CGImageRelease(partialImageRef);

                    //获取绘制的图片
                    partialImageRef = CGBitmapContextCreateImage(bmContext);
                    CGContextRelease(bmContext);
                }
                else {
                    CGImageRelease(partialImageRef);
                    partialImageRef = nil;
                }
            }
#endif

            if (partialImageRef) {

                //转化image
                UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
                //获取图片的key 其实就是url
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                //根据屏幕的大小(使用@2x或@3x) 对图片进行处理
                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)connectionDidFinishLoading:(NSURLConnection *)aConnection {

    //接收数据完成后,completionBlock
    SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
    @synchronized(self) {
        //线程加锁 停止runloop 当前线程置nil,连接置nil,主线程中发送异步通知
        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];
        });
    }


    // 发送的request,服务器会返回一个response,就像获取服务器端的图片一样,
    // 如果图片没有改变,第二次获取的时候,最好直接从缓存中获取,这会省不少时间。
    // response也一样,也弄一个缓存,就是NSURLCache。
    // 根据你的request,看看是不是缓存中能直接获取到对应的response。
    if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
        // 为NO表示没有从NSURLCache中获取到response
        responseFromCached = NO;
    }

    if (completionBlock) {
        //如果为忽略cache或从缓存中取request失败
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) {
            completionBlock(nil, nil, nil, YES);
        } else if (self.imageData) {
            //根据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];
}

发生错误的代理

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

// 如果我们需要对缓存做更精确的控制,我们可以实现一些代理方法来允许应用来确定请求是否应该缓存
// 如果不实现此方法,NSURLConnection 就简单地使用本来要传入 -connection:willCacheResponse: 的那个缓存对象,
所以除非你需要改变一些值或者阻止缓存,否则这个代理方法不必实现

- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse {
    // 如果该方法被调用,说明该Response不是从cache读取的,因为会会响应该方法,说明这个cacheResponse是刚从服务端获取的新鲜Response,需要进行缓存。
    responseFromCached = NO; // If this method is called, it means the response wasn't read from cache
    if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
        // Prevents caching of responses
        // 如果request的缓存策略是NSURLRequestReloadIgnoringLocalCacheData,就不缓存了
        return nil;
    }
    else {
        return cachedResponse;
    }
}

认证相关的东东
// 当客户端向目标服务器发送请求时。服务器会使用401进行响应。客户端收到响应后便开始认证挑战(Authentication Challenge),而且是通过willSendRequestForAuthenticationChallenge:函数进行的。
// willSendRequestForAuthenticationChallenge:函数中的challenge对象包含了protectionSpace(NSURLProtectionSpace)实例属性,在此进行protectionSpace的检查。当检查不通过时既取消认证,这里需要注意下的是取消是必要的,因为willSendRequestForAuthenticationChallenge:可能会被调用多次。

- (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{

    // NSURLProtectionSpace主要有Host、port、protocol、realm、authenticationMethod等属性。
    // 为了进行认证,程序需要使用服务端期望的认证信息创建一个NSURLCredential对象。我们可以调用authenticationMethod来确定服务端的认证方法,这个认证方法是在提供的认证请求的保护空间(protectionSpace)中。
    // 服务端信任认证(NSURLAuthenticationMethodServerTrust)需要一个由认证请求的保护空间提供的信任。使用credentialForTrust:来创建一个NSURLCredential对象。
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {

        // SDWebImageDownloaderAllowInvalidSSLCertificates表示允许不受信任SSL认证
        // 注释中提示尽量作为test使用,不要在最终production使用。
        // 所以此处使用performDefaultHandlingForAuthenticationChallenge,即使用系统提供的默认行为

        if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates) &&
            [challenge.sender respondsToSelector:@selector(performDefaultHandlingForAuthenticationChallenge:)]) {
            [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge];
        } else {
            NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
        }
    } else {
        // 每次认证失败,previousFailureCount就会加1
        // 第一次认证(previousFailureCount == 0)并且有Credential,使用Credential认证
        // 非第一次认证或者第一次认证没有Credential,对于认证挑战,不提供Credential就去download一个request,但是如果这里challenge是需要Credential的challenge,那么使用这个方法是徒劳的
        if ([challenge previousFailureCount] == 0) {
            if (self.credential) {
                //为 challenge 的发送方提供 credential
                [[challenge sender] useCredential:self.credential forAuthenticationChallenge:challenge];
            } else {
                [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
            }
        } else {
            [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
        }
    }
}

以上为下载image 的代码,重要代码解析都在上面,这个大家应该比较熟悉,经过前几篇的介绍,这些代码的阅读应该不会很困难。
下一篇我们将做扫尾工作,将SDWebimage中的其它常用的类和方法在过一遍,这样我们对SDWebimage的理解会更加深刻。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值