IOS SDWebImage 2.X源码阅读(三)

前言:
IOS SDWebImage 2.X源码阅读(一)
IOS SDWebImage 2.X源码阅读(二)
IOS SDWebImage 2.X源码阅读(三)
IOS SDWebImage 2.X源码阅读(四)

(5)真正到了下载图片的相关代码了………………

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];

看下NSMutableURLRequest相关的概念
NSURLRequestCachePolicy缓存策略的枚举值,相关概念参考

typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
    NSURLRequestUseProtocolCachePolicy = 0,//对特定的 URL 请求使用网络协议中实现的缓存逻辑。这是默认的策略。------默认行为

    NSURLRequestReloadIgnoringLocalCacheData = 1,//数据需要从原始地址加载。不使用现有缓存。------不使用缓存
    NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented不仅忽略本地缓存,同时也忽略代理服务器或其他中间介质目前已有的、协议允许的缓存(未实现)
    NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,

    NSURLRequestReturnCacheDataElseLoad = 2,//无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么从原始地址加载数据。------使用缓存(不管它是否过期),如果缓存中没有,那从网络加载吧
    NSURLRequestReturnCacheDataDontLoad = 3,//无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么放弃从原始地址加载数据,请求视为失败(即:“离线”模式)。------离线模式:使用缓存(不管它是否过期),但是不从网络加载

    NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented从原始地址确认缓存数据的合法性后,缓存数据就可以使用,否则从原始地址加载。(未实现)
};
 /*YES:URL loading system会自动为NSURLRequest发送合适的存储cookie。从NSURLResponse返回的cookie也会根据当前的cookie访问策略(cookie acceptance policy)接收到系统中。
 NO:不使用cookie
 */
 request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);

 /* HTTPShouldUsePipelining表示receiver(理解为iOS客户端)的下一个信息是否必须等到上一个请求回复才能发送。
   如果为YES表示可以,NO表示必须等receiver收到先前的回复才能发送下个信息。
*/
 request.HTTPShouldUsePipelining = YES;

  /*
#ifdef SD_WEBP
        _HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
        _HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
*/
 if (wself.headersFilter) {
    request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else {
    request.allHTTPHeaderFields = wself.HTTPHeaders;
}

下面调用SDWebImageDownloaderOperation类中的initWithRequest方法,网络请求数据,下载图片,这个等下看,先看下面的operation属性设置

//解压下载和缓存的图像可以提高性能,但会占用大量内存。
operation.shouldDecompressImages = wself.shouldDecompressImages;//yes

web 服务可以在返回 http 响应时附带认证要求的challenge,作用是询问 http 请求的发起方是谁,这时发起方应提供正确的用户名和密码(即认证信息),然后 web 服务才会返回真正的 http 响应。

if (wself.urlCredential) {
      operation.credential = wself.urlCredential;
 } else if (wself.username && wself.password) {
       operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}

设置队列的优先级

if (options & SDWebImageDownloaderHighPriority) {
       operation.queuePriority = NSOperationQueuePriorityHigh;
  } else if (options & SDWebImageDownloaderLowPriority) {
       operation.queuePriority = NSOperationQueuePriorityLow;
  }

将operation 实例加入到 NSOperationQueue 中,就会调用start(),等会看start方法

[wself.downloadQueue addOperation:operation];

NSOperation之间可以设置依赖来保证执行顺序,⽐如一定要让操作A执行完后,才能执行操作B,可以像下面这么写

[operationB addDependency:operationA]; // 操作B依赖于操作A

if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {//下载操作以堆栈样式执行(后进先出)。
       [wself.lastAddedOperation addDependency:operation];
       wself.lastAddedOperation = operation;
}

看下SDWebImageDownloaderOperation类中的下载图片的方法,都是初始化的一些方法

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

NSOperation是个抽象类,并不具备分装操作的能力,必须使用它的子类
1、NSInvocationOperation
2、NSBlockOperation
3、自定义子类继承自NSOperation,实现内部相应的方法
下载图片的SDWebImageDownloaderOperation就是自定义继承NSOperation类,并实现了相关的方法,关于自定义NSOPeration的可查看该文章

/* 
 *自定义并发的NSOperation需要以下步骤: 
 1.start方法:该方法必须实现, 
 2.main:该方法可选,如果你在start方法中定义了你的任务,则这个方法就可以不实现,但通常为了代码逻辑清晰,通常会在该方法中定义自己的任务 
 3.isExecuting  isFinished 主要作用是在线程状态改变时,产生适当的KVO通知 
 4.isConcurrent :必须覆盖并返回YES; 
 */  

- (void)setFinished:(BOOL)finished {
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

- (void)setExecuting:(BOOL)executing {
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

- (BOOL)isConcurrent {
    return YES;
}

现在来看下start()方法,一进去该方法就是一个线程线程同步锁,检查当前的operation是否取消了,若是取消了,标示当前任务已完成,并将相关的信息reset,相关的都置nil,然后初始化一个NSURLConnection

 @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

然后是当app进入后台之后,该如何操作

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

1、 NSClassFromString():根据字符串名称获取同名的类
2、 [self shouldContinueWhenAppEntersBackground]:SDWebImageDownloaderOperation设置SDWebImageContinueInBackground属性
3、 beginBackgroundTaskWithExpirationHandler后面的block,会在app进入后台之后,如果在系统规定时间内任务还没有完成,在时间到之前会调用到这个方法
4、必须调用endBackgroundTask:方法来结束使用beginBackgroundTaskWithExpirationHandler:方法开始的任务。 如果你不这样做,系统可能会终止你的应用程序。
该方法可以安全地在非主线程上调用。
5、若是在指定的时间内任务未完成,先调用cancle方法

 @synchronized (self) {
         //………………………………………………………………
         self.executing = YES;
        //ios7以后就不再使用NSURLConnection,使用NSURLSession代替,我们现在的版本是3.X
         /*
         第一个参数:请求对象
         第二个参数:谁成为NSURLConnetion对象的代理
         第三个参数:是否马上发送网络请求,如果该值为YES则立刻发送,如果为NO则不会发送网路请求
          设置回调方法也在子线程中运行,在子线程中默认是没有runloop,需添加一个 RunLoop 到当前的线程中来,
         */
        self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
        self.thread = [NSThread currentThread];
   }

执行[connection start]开始网络请求

 //在startImmediately为NO时,调用该方法控制网络请求的发送
    [self.connection start];

由于我们是在子线程中运行,那么它调用的委托方法也是在对应的子线程中执行,其委托方法会不断接收到下载的数据,为了防止子线程被kill掉,在该子线程中添加一个runloop,让该线程一直在等待下载结束,被手动kill

 AppLog(@"开始下载前 runloop before");
        if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
        }
        else {
            CFRunLoopRun();
        }
  AppLog(@"下载完成 runloop after");

看下NSURLConnectionDataDelegate的几个方法

/*
 1.当接收到服务器响应的时候调用,该方法只会调用一次
 第一个参数connection:监听的是哪个NSURLConnection对象
 第二个参数response:接收到的服务器返回的响应头信息
 */
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    //........
}

看下该方法中的相关代码,首先判断响应头中的状态码

  //是否实现了statusCode方法 || (服务器返回的响应头的状态码 < 400 && statusCode !=304 )
 if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304))
//预计接收的数据大小,调用下载进度的block
 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];
        });

看下刚才响应头的statusCode的else情况的代码,即我们正常情况下不成功的情况

 NSUInteger code = [((NSHTTPURLResponse *)response) statusCode];
        if (code == 304) {//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());//手动停止当前runloop,退出去
        [self done];

继续看下NSURLConnectionDataDelegate委托方法connection:didReceiveData:data,该方法是多次调用,直到数据下载完成

/*
 2.当接收到数据的时候调用,该方法会被调用多次
 第一个参数connection:监听的是哪个NSURLConnection对象
 第二个参数data:本次接收到的服务端返回的二进制数据(可能是片段)
 */
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    //.........
    }

将请求到的数据添加到self.imageData变量中

[self.imageData appendData:data];

下载设置了SDWebImageDownloaderProgressiveDownload属性 && 下载数据的预计大小 > 0 && self.completedBlock

 if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {}

看下上面if里面的代码

     //当前接收到数据的大小
        const NSInteger totalSize = self.imageData.length;

    //更新数据源,我们必须传递所有的数据,而不仅仅是新的字节
    CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
        //第一次进来时,会执行如下
        if (width + height == 0) {
            CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);//从source里面读取各个图片放入数组里面。
            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); 
                //当我们绘制到Core Graphics绘制image时,我们会失去了图片方向的信息,这意味着initWithCGIImage所生成的下面的图像有时会被错误地定位。 (与connectionDidFinishLoading中的initWithData所生成的图像不同。)因此,将图片的方向信息保存在此处并稍后传递。
                orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
            }

        }

然后图片的width 和 height获取到值了

if (width + height > 0 && totalSize < self.expectedSize) 图片宽高 > 0 && 当前下载数据大小 < 预计数据大小

该if条件下有两个if判断,我们先看第一个在,iOS

// Create the image
            CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);

#ifdef TARGET_OS_IPHONE
            // Workaround for iOS anamorphic image iOS变形图像的解决方法
            if (partialImageRef) {
                const size_t partialHeight = CGImageGetHeight(partialImageRef);
                //RGBA 色彩 (显示3色)
                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判断

 if (partialImageRef) {
                UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                AppLog(@"connection key-->%@",key);
                //根据key从内存缓存中查找图片
                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) {
                        //此时图片还未下载完成,所以为no
                        self.completedBlock(image, nil, nil, NO);
                    }
                });
            }

继续调用用户自定义的progressBlock

if (self.progressBlock) {
        self.progressBlock(self.imageData.length, self.expectedSize);
    }

继续看下NSURLConnectionDataDelegate委托方法connectionDidFinishLoading,该方法是在服务端返回的数据接收完毕之后会调用

/*
 3.当服务端返回的数据接收完毕之后会调用
 通常在该方法中解析服务器返回的数据
 */
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
    AppLog(@"数据请求完成!connectionDidFinish")
    //加锁
    @synchronized(self) {
        CFRunLoopStop(CFRunLoopGetCurrent());//手动停止runloop
        //回收资源
        self.thread = nil;
        self.connection = nil;
        }
    }
    //发送的request,服务器会返回一个响应的response,我们加载图片时,如果图片没有改变,可以直接从缓存中获取,其实response也是一样的,也有个NSURLCache,根据request,看看是不是命中缓存
     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];
            AppLog(@"key(SDWebImageDownloaderOperation)-->%@",key);
            AppLog(@"对图片进行相关缩放处理");
            image = [self scaledImageForKey:key image:image];//对图片进行相关缩放处理
            //gif图片(是由多张图片构成的)不需要解压缩
            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];
   }

看下对下载图片的相关处理,在上面的方法中,有调用了sd_imageWithData:data方法

+ (UIImage *)sd_imageWithData:(NSData *)data {
    if (!data) {
        return nil;
    }

    UIImage *image;
    //根据NSData的前几个字节就能判断图片的类型,jpeg,png,gif,tiff,webp
    NSString *imageContentType = [NSData sd_contentTypeForImageData:data];//图片类型
    AppLog(@"imageContentType-->%@",imageContentType);
    if ([imageContentType isEqualToString:@"image/gif"]) {
        image = [UIImage sd_animatedGIFWithData:data];
    }
#ifdef SD_WEBP
    else if ([imageContentType isEqualToString:@"image/webp"])
    {
        image = [UIImage sd_imageWithWebPData:data];
    }
#endif
    else {
        image = [[UIImage alloc] initWithData:data];
        UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];//图片方向
        if (orientation != UIImageOrientationUp) {
            image = [UIImage imageWithCGImage:image.CGImage
                                        scale:image.scale
                                  orientation:orientation];
        }
    }


    return image;
}

根据NSData的前几个字节就能判断图片的类型,jpeg,png,gif,tiff,webp等
但是在这个版本的SDWebImage中,没有对webp类型的图片,进行处理

+ (NSString *)sd_contentTypeForImageData:(NSData *)data {
    uint8_t c;
    [data getBytes:&c length:1];
    switch (c) {
        case 0xFF:
            return @"image/jpeg";
        case 0x89:
            return @"image/png";
        case 0x47:
            return @"image/gif";
        case 0x49:
        case 0x4D:
            return @"image/tiff";
        case 0x52:
            // R as RIFF for WEBP
            if ([data length] < 12) {
                return nil;
            }

            NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
            if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                return @"image/webp";
            }

            return nil;
    }
    return nil;
}

上面是对connection连接成功的相关处理,现在看下连接失败的delegate的方法

//连接失败
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    AppLog(@"连接失败");
    AppLog(@"error-->%@",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];
}

猜你喜欢

转载自blog.csdn.net/ycf03211230/article/details/79573317