huang303513

huang303513 查看完整档案

上海编辑西北工业大学  |  软件工程 编辑携程  |  iOS 编辑 huang303513.github.io/ 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

huang303513 发布了文章 · 2017-05-03

SDWebImage源码解析(一)

1 概述

SDWebImage基本是iOS项目的标配。他以灵活简单的api,提供了图片从加载、解析、处理、缓存、清理等一些列功能。让我们专心于业务的处理。但是并不意味着会用就可以了,通过源码分析和学习,让我们知道如何用好它。学习分析优秀源码也可以从潜移默化中给我们提供很多解决日常需求的思路。下面就是一张图来概述SDWebImage的所有类:

img

通过对这个图片的分析,我们可以把SDWebImage的源码分为三种:

  • 各种分类:

    • UIButton(WebCache)UIButton类添加载图片的方法。比如正常情况下、点击情况下、的image属性和背景图片等。

    • MKAnnotationView(WebCache)MKAnnotationView类添加各种加载图片的方法。

    • UIImageView(WebCache)UIImageView类添加加载图片的方法。

    • UIImageView(HighlightedWebCache)UIImageView类添加高亮状态下加载图片的方法。

    • FLAnimatedImageView(WebCache)FLAnimatedImageView类添加加载动态的方法,这个分类需要引入FLAnimatedImage框架。SDWebImage推荐使用这个框架来处理动态图片(GIF)的加载。

    • UIImageView、UIButton、FLAnimatedImageView通过sd_setImageWithURL等api来做图片加载请求。这也是我们唯一需要做的。

    • 上面的几个UIView子类都会调用UIView(WebCache)分类的sd_internalSetImageWithURL方法来做图片加载请求。具体是通过SDWebImageManager调用来实现的。同时实现了Operation取消、ActivityIndicator的添加与取消。

  • 各种工具类:

    • NSData+ImageContentType: 根据图片数据获取图片的类型,比如GIF、PNG等。

    • SDWebImageCompat: 根据屏幕的分辨倍数成倍放大或者缩小图片大小。

    • SDImageCacheConfig: 图片缓存策略记录。比如是否解压缩、是否允许iCloud、是否允许内存缓存、缓存时间等。默认的缓存时间是一周。

    • UIImage+MultiFormat: 获取UIImage对象对应的data、或者根据data生成指定格式的UIImage,其实就是UIImage和NSData之间的转换处理。

    • UIImage+GIF: 对于一张图片是否GIF做判断。可以根据NSData返回一张GIF的UIImage对象,并且只返回GIF的第一张图片生成的GIF。如果要显示多张GIF,使用FLAnimatedImageView

    • SDWebImageDecoder: 根据图片的情况,做图片的解压缩处理。并且根据图片的情况决定如何处理解压缩。

  • 核心类:

    • SDImageCache: 负责SDWebImage的整个缓存工作,是一个单列对象。缓存路径处理、缓存名字处理、管理内存缓存和磁盘缓存的创建和删除、根据指定key获取图片、存入图片的类型处理、根据缓存的创建和修改日期删除缓存。

    • SDWebImageManager: 拥有一个SDWebImageCacheSDWebImageDownloader属性分别用于图片的缓存和加载处理。为UIView及其子类提供了加载图片的统一接口。管理正在加载操作的集合。这个类是一个单列。还有就是各种加载选项的处理。

    • SDWebImageDownloader: 实现了图片加载的具体处理,如果图片在缓存存在则从缓存区。如果缓存不存在,则直接创建一个。SDWebImageDownloaderOperation对象来下载图片。管理NSURLRequest对象请求头的封装、缓存、cookie的设置。加载选项的处理等功能。管理Operation之间的依赖关系。这个类是一个单列.

    • SDWebImageDownloaderOperation: 一个自定义的并行Operation子类。这个类主要实现了图片下载的具体操作、以及图片下载完成以后的图片解压缩、Operation生命周期管理等。

    • UIView+WebCache: 所有的UIButton、UIImageView都回调用这个分类的方法来完成图片加载的处理。同时通过UIView+WebCacheOperation分类来管理请求的取消和记录工作。所有UIView及其子类的分类都是用这个类的sd_intemalSetImageWithURL:来实现图片的加载。

    • FLAnimatedImageView: 动态图片的数据通过ALAnimatedImage对象来封装。FLAnimatedImageViewUIImageView的子类。通过他完全可以实现动态图片的加载显示和管理。并且比UIImageView做了流程优化。

2 实现流程

SDWebImage为我们实现了图片加载、数据处理、图片缓存等一些列工作。通过下图我们可以分析一下他的流程:

img

通过这个图,我们发现SDWebImage加载的过程是首先从缓存中加载数据。而且缓存加载又是优先从内存缓存中加载,然后才是磁盘加载。最后如果缓存没有,才从网络上加载。同时网络成功加载图片以后,存入本地缓存。

3 UIView+WebCache分析

UIImageView、UIButton、FLAnimatedImageView都会调用UIView(WebCache)分类的sd_internalSetImageWithURL方法来做图片加载请求。具体是通过SDWebImageManager调用来实现的。同时实现了Operation取消、ActivityIndicator的添加与取消。我们首先来看sd_internalSetImageWithURL方法的实现:

/**
 所有UIView及其子类都是通过这个方法来加载图片

 @param url 加载的url
 @param placeholder 占位图
 @param options 加载选项
 @param operationKey key
 @param setImageBlock Block
 @param progressBlock 进度Block
 @param completedBlock 回调Block
 */
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock {
    
    //取消当前类所对应的所有下载Operation对象
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    /*
     把UIImageView的加载图片操作和他自身用关联对象关联起来,方便后面取消等操作。关联的key就是UIImageView对应的类名
     */
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //如果有设置站位图,则先显示站位图
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    if (url) {
        // check if activityView is enabled or not
        //如果UIImageView对象有设置添加转动菊花数据,加载的时候添加转动的菊花
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
        
        __weak __typeof(self)wself = self;
        /*
         *operation是一个`SDWebImageCombinedOperation`对象。通过这个对象来获取图片
         */
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong __typeof (wself) sself = wself;
            //停止菊花
            [sself sd_removeActivityIndicator];
            if (!sself) {
                return;
            }
            dispatch_main_async_safe(^{
                if (!sself) {
                    return;
                }
                //如果设置了不自动显示图片,则直接调用completedBlock,让调用者处理图片的显示
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                    completedBlock(image, error, cacheType, url);
                    return;
                } else if (image) {
                    //自动显示图片
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    [sself sd_setNeedsLayout];
                } else {
                    //如果设置了延迟显示占位图,则图片加载失败的情况下显示占位图
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                //完成回调
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        //关联Operationkey与Operation对象。方便后面根据key取消operation操作等。
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
        //加载失败的情况
        dispatch_main_async_safe(^{
            //移除菊花
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

给UIView及其子类添加旋转菊花是通过关联对象来实现的。通过如下几个方法来实现:

#pragma mark 通过关联对象来实现菊花的添加
- (UIActivityIndicatorView *)activityIndicator {
    return (UIActivityIndicatorView *)objc_getAssociatedObject(self, &TAG_ACTIVITY_INDICATOR);
}

- (void)setActivityIndicator:(UIActivityIndicatorView *)activityIndicator {
    objc_setAssociatedObject(self, &TAG_ACTIVITY_INDICATOR, activityIndicator, OBJC_ASSOCIATION_RETAIN);
}
#pragma mark 是否显示旋转菊花
- (void)sd_setShowActivityIndicatorView:(BOOL)show {
    objc_setAssociatedObject(self, &TAG_ACTIVITY_SHOW, @(show), OBJC_ASSOCIATION_RETAIN);
}

- (BOOL)sd_showActivityIndicatorView {
    return [objc_getAssociatedObject(self, &TAG_ACTIVITY_SHOW) boolValue];
}
#pragma mark 旋转菊花的样式
- (void)sd_setIndicatorStyle:(UIActivityIndicatorViewStyle)style{
    objc_setAssociatedObject(self, &TAG_ACTIVITY_STYLE, [NSNumber numberWithInt:style], OBJC_ASSOCIATION_RETAIN);
}

- (int)sd_getIndicatorStyle{
    return [objc_getAssociatedObject(self, &TAG_ACTIVITY_STYLE) intValue];
}

还有就是通过UIView+WebCacheOperation类来实现UIView的图片下载Operation的关联和取消。具体key的值可以从sd_internalSetImageWithURL中找到具体获取方式,通过在这个方法中实现Operation的关联与取消。

/**
 关联Operation对象与key对象

 @param operation Operation对象
 @param key key
 */
- (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key {
    if (key) {
        [self sd_cancelImageLoadOperationWithKey:key];
        if (operation) {
            SDOperationsDictionary *operationDictionary = [self operationDictionary];
            operationDictionary[key] = operation;
        }
    }
}
/**
 取消当前key对应的所有实现了SDWebImageOperation协议的Operation对象

 @param key Operation对应的key
 */
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    // Cancel in progress downloader from queue
    //获取当前View对应的所有key
    SDOperationsDictionary *operationDictionary = [self operationDictionary];
    //获取对应的图片加载Operation
    id operations = operationDictionary[key];
    //取消所有当前View对应的所有Operation
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id <SDWebImageOperation> operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id<SDWebImageOperation>) operations cancel];
        }
        [operationDictionary removeObjectForKey:key];
    }
}

4 FLAnimatedImageView分析

SDWebImage使用FLAnimatedImage框架来处理动态图片,它包含FLAnimatedImageFLAnimatedImageView两个雷。动态图片的数据通过ALAnimatedImage对象来封装。FLAnimatedImageViewUIImageView的子类。通过他完全可以实现动态图片的加载显示和管理。并且比UIImageView做了流程优化。我们来看一下FLAnimatedImageView.h里面定义的接口:

/**
 `FLAnimatedImageView`是一个`UIImageView`的子类。实现了`UIImageView`的`start/stop/isAnimating`方法。所以我们可以直接使用`FLAnimatedImageView`替代`UIImageView`。
 通过`CADisplayLink`对象来处理当前图片帧和下一帧图片的显示。
 */
@interface FLAnimatedImageView : UIImageView
/**
 动态图片的封装对象。首先通过设置`[UIImageView.image]`为nil来清除已经存在的动态图片。设置`animatedImage`属性会自动设置新的动态图片并且开始显示。而且会把当前显示的UIImage存入`currentFrame`中。
 */
@property (nonatomic, strong) FLAnimatedImage *animatedImage;

@property (nonatomic, copy) void(^loopCompletionBlock)(NSUInteger loopCountRemaining);
/**
 当前动画帧对应的UIImage对象
 */
@property (nonatomic, strong, readonly) UIImage *currentFrame;
/**
 当前图片镇对应的索引
 */
@property (nonatomic, assign, readonly) NSUInteger currentFrameIndex;
/**
 指定动态图片执行所在的runloop的mode。NSRunLoopCommonMode
 */
@property (nonatomic, copy) NSString *runLoopMode;
@end

我们通过FLAnimatedImageView+WebCache这个分类的sd_setImageWithURL来加载动态图片:

/**
 FLAnimatedImage+WebCache分类通过这个方法来加载动态图片

 @param url 图片的url
 @param placeholder 占位图
 @param options 加载选项
 @param progressBlock 进度Block
 @param completedBlock 完成Block
 */
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock {
    __weak typeof(self)weakSelf = self;
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:nil
                       setImageBlock:^(UIImage *image, NSData *imageData) {
                           //根据NSData的类型获取图片的类型
                           SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
                           //如果是GIF,则处理
                           if (imageFormat == SDImageFormatGIF) {
                               //给FLAnimatedImageView的animatedImage属性设置动态图片。这个setter方法被重写了
                               weakSelf.animatedImage = [FLAnimatedImage animatedImageWithGIFData:imageData];
                               weakSelf.image = nil;
                           } else {
                               //不是动态图片,则正常显示
                               weakSelf.image = image;
                               weakSelf.animatedImage = nil;
                           }
                       }
                            progress:progressBlock
                           completed:completedBlock];
}

从上面可以看出,获取图片数据以后。首先通过SDImageFormat得到图片的类型。如果是GIF类型,则先把图片数据封装成一个FLAnimatedImage对象。然后设置给animatedImage属性。这个属性的setter方法如下:

/**
 animatedImage的setter方法。通过这个属性setter方法来设置FLAnimatedImageView的数据。并且开始动态显示

 @param animatedImage animatedImage属性
 */
- (void)setAnimatedImage:(FLAnimatedImage *)animatedImage
{
    if (![_animatedImage isEqual:animatedImage]) {
        if (animatedImage) {
            //清除UIImageView以前的图片数据
            super.image = nil;
            super.highlighted = NO;
            //先说intrinsicContentSize,也就是控件的内置大小。比如UILabel,UIButton等控件,他们都有自己的内置大小。控件的内置大小往往是由控件本身的内容所决定的,比如一个UILabel的文字很长,那么该UILabel的内置大小自然会很长。控件的内置大小可以通过UIView的intrinsicContentSize属性来获取内置大小,也可以通过invalidateIntrinsicContentSize方法来在下次UI规划事件中重新计算intrinsicContentSize。如果直接创建一个原始的UIView对象,显然它的内置大小为0。
            [self invalidateIntrinsicContentSize];
        } else {
            //停止动态图片的动态显示
            [self stopAnimating];
        }
        //赋值
        _animatedImage = animatedImage;
        //当前动态图片数据帧
        self.currentFrame = animatedImage.posterImage;
        //当前数据帧索引
        self.currentFrameIndex = 0;
        if (animatedImage.loopCount > 0) {
            self.loopCountdown = animatedImage.loopCount;
        } else {
            self.loopCountdown = NSUIntegerMax;
        }
        self.accumulator = 0.0;
        
        //更新对象的状态。从而更新shouldAnimated这个属性的值。
        [self updateShouldAnimate];
        if (self.shouldAnimate) {
            //开始动态显示
            [self startAnimating];
        }
        
        [self.layer setNeedsDisplay];
    }
}
/**
 判断当前FLAnimatedImageView是否需要显示动画
 */
- (void)updateShouldAnimate
{
    BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0;
    self.shouldAnimate = self.animatedImage && isVisible;
}

5 CADisplayLink

有趣的地方是FLAnimatedImageView通过过CADisplayLink来刷新动态图片帧的显示。CADisplayLink是一个能让我们以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。我们在应用中创建一个新的CADisplayLink对象,把它添加到一个runloop中,并给它提供一个target和selector在屏幕刷新的时候调用。

一但CADisplayLink以特定的模式注册到runloop之后,每当屏幕需要刷新的时候runloop就会调用CADisplayLink绑定的target上的selector,这时target可以读到CADisplayLink的每次调用的时间戳,用来准备下一帧显示需要的数据。例如一个视频应用使用时间戳来计算下一帧要显示的视频数据。在UI做动画的过程中,需要通过时间戳来计算UI对象在动画的下一帧要更新的大小等等。在添加进runloop的时候我们应该选用高一些的优先级,来保证动画的平滑。可以设想一下,我们在动画的过程中,runloop被添加进来了一个高优先级的任务,那么,下一次的调用就会被暂停转而先去执行高优先级的任务,然后在接着执行CADisplayLink的调用,从而造成动画过程的卡顿,使动画不流畅。duration属性提供了每帧之间的时间,也就是屏幕每次刷新之间的的时间。我们可以使用这个时间来计算出下一帧要显示的UI的数值。但是duration只是个大概的时间,如果CPU忙于其它计算,就没法保证以相同的频率执行屏幕的绘制操作,这样会跳过几次调用回调方法的机会。frameInterval属性是可读可写的NSInteger型值,标识间隔多少帧调用一次selector方法,默认值是1,即每帧都调用一次。如果每帧都调用一次的话,对于iOS设备来说那刷新频率就是60HZ也就是每秒60次,如果将 frameInterval 设为2 那么就会两帧调用一次,也就是变成了每秒刷新30次。我们通过pause属性开控制CADisplayLink的运行。当我们想结束一个CADisplayLink的时候,应该调用-(void)invalidate从runloop中删除并删除之前绑定的 target跟selector。另外CADisplayLink 不能被继承。

//每1/60秒都回调用一次displayDidRefresh方法来做UI处理
self.displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayDidRefresh:)];
//把displayLink加入主线程的commomMode里面        
[self.displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode];

最后原文地址.html),demo地址

查看原文

赞 0 收藏 3 评论 0

huang303513 发布了文章 · 2017-05-03

SDWebImage源码解析(二)

1 概述

SDWebImage使用了很多工具类来对图片的处理。比如获取图片类型、图片放大缩小、GIF图片处理、图片解压缩处理等。接下来我就要分析下面这几个工具类的实现。

2 NSData+ImageContentType分析

这个类提供了一个类方法sd_imageFormatForImageData。通过这个方法传入图片的NSData数据,然后返回图片类型。图片类型通过SDImageFormat来定义。

/**
 不同图片类型的枚举

 - SDImageFormatUndefined: 未知
 - SDImageFormatJPEG: JPG
 - SDImageFormatPNG: PNG
 - SDImageFormatGIF: GIF
 - SDImageFormatTIFF: TIFF
 - SDImageFormatWebP: WEBP  
 */
typedef NS_ENUM(NSInteger, SDImageFormat) {
    SDImageFormatUndefined = -1,
    SDImageFormatJPEG = 0,
    SDImageFormatPNG,
    SDImageFormatGIF,
    SDImageFormatTIFF,
    SDImageFormatWebP
};
/**
 根据图片NSData获取图片的类型

 @param data NSData数据
 @return 图片数据类型
 */
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
    if (!data) {
        return SDImageFormatUndefined;
    }
    
    uint8_t c;
    //获取图片数据的第一个字节数据
    [data getBytes:&c length:1];
    //根据字母的ASC码比较
    switch (c) {
        case 0xFF:
            return SDImageFormatJPEG;
        case 0x89:
            return SDImageFormatPNG;
        case 0x47:
            return SDImageFormatGIF;
        case 0x49:
        case 0x4D:
            return SDImageFormatTIFF;
        case 0x52:
            // R as RIFF for WEBP
            if (data.length < 12) {
                return SDImageFormatUndefined;
            }
            
            NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
            if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                return SDImageFormatWebP;
            }
    }
    return SDImageFormatUndefined;
}

3 SDWebImageCompat分析

SDWebImageCompat就提供一个全局方法SDScaledImageForKey。这个方法根据原始图片绘制一张放大或者缩小的图片。

/**
 给定一张图片,通过scale属性返回一个放大的图片。

 @param key 图片名称
 @param image 资源图片
 @return 处理以后的图片
 */
inline UIImage *SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image) {
    //异常处理
    if (!image) {
        return nil;
    }
#if SD_MAC
    return image;
#elif SD_UIKIT || SD_WATCH
    //如果是动态图片,比如GIF图片,则迭代处理
    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
            CGFloat scale = 1;
            // “@2x.png”的长度为7,所以此处添加了这个判断,很巧妙
            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 *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
            image = scaledImage;
        }
        return image;
    }
#endif
}

4 UIImage+MultiFormat分类分析

UIImage+MultiFormat分类实现了NSData与UIImage对象之间的相互转换。并且是根据图片类型做转换。比如GIF的UIImage转换为GIF格式的NSData。
并且还有UIImage的Orientation和alpha的处理。

/**
 根据image的data数据。生成对应的image对象

 @param data 图片的数据
 @return image对象
 */
+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data {
    if (!data) {
        return nil;
    }
    UIImage *image;
    //获取data的图片类型,png,gif,jpg
    SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:data];
    if (imageFormat == SDImageFormatGIF) {
        //gif处理:返回一张只包含数据第一张image 的gif图片
        image = [UIImage sd_animatedGIFWithData:data];
    }
#ifdef SD_WEBP
    else if (imageFormat == SDImageFormatWebP)
    {
        image = [UIImage sd_imageWithWebPData:data];
    }
#endif
    else {
        image = [[UIImage alloc] initWithData:data];
#if SD_UIKIT || SD_WATCH
        //获取方向
        UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];
        //如果不是向上的,还需要再次生成图片
        if (orientation != UIImageOrientationUp) {
            image = [UIImage imageWithCGImage:image.CGImage
                                        scale:image.scale
                                  orientation:orientation];
        }
#endif
    }


    return image;
}

#if SD_UIKIT || SD_WATCH

/**
 根据图片数据获取图片的方向

 @param imageData 图片数据
 @return 方向
 */
+(UIImageOrientation)sd_imageOrientationFromImageData:(nonnull NSData *)imageData {
    //默认是向上的
    UIImageOrientation result = UIImageOrientationUp;
    CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
    if (imageSource) {
        //获取图片的属性列表
        CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
        if (properties) {
            CFTypeRef val;
            int exifOrientation;
            //获取图片方向
            val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
            if (val) {
                CFNumberGetValue(val, kCFNumberIntType, &exifOrientation);
                result = [self sd_exifOrientationToiOSOrientation:exifOrientation];
            } // else - if it's not set it remains at up
            CFRelease((CFTypeRef) properties);
        } else {
            //NSLog(@"NO PROPERTIES, FAIL");
        }
        CFRelease(imageSource);
    }
    return result;
}
/**
 根据不同的值返回不同的图片方向

 @param exifOrientation 输入值
 @return 图片的方向
 */
+ (UIImageOrientation) sd_exifOrientationToiOSOrientation:(int)exifOrientation {
    UIImageOrientation orientation = UIImageOrientationUp;
    switch (exifOrientation) {
        case 1:
            orientation = UIImageOrientationUp;
            break;

        case 3:
            orientation = UIImageOrientationDown;
            break;

        case 8:
            orientation = UIImageOrientationLeft;
            break;

        case 6:
            orientation = UIImageOrientationRight;
            break;

        case 2:
            orientation = UIImageOrientationUpMirrored;
            break;

        case 4:
            orientation = UIImageOrientationDownMirrored;
            break;

        case 5:
            orientation = UIImageOrientationLeftMirrored;
            break;

        case 7:
            orientation = UIImageOrientationRightMirrored;
            break;
        default:
            break;
    }
    return orientation;
}
#endif

- (nullable NSData *)sd_imageData {
    return [self sd_imageDataAsFormat:SDImageFormatUndefined];
}
/**
 根据指定的图片类型,把image对象转换为对应格式的data
 
 @param imageFormat 指定的image格式
 @return 返回data对象
 */
- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat {
    NSData *imageData = nil;
    if (self) {
#if SD_UIKIT || SD_WATCH
        int alphaInfo = CGImageGetAlphaInfo(self.CGImage);
        //是否有透明度
        BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                          alphaInfo == kCGImageAlphaNoneSkipFirst ||
                          alphaInfo == kCGImageAlphaNoneSkipLast);
        //只有png图片有alpha属性
        BOOL usePNG = hasAlpha;
        
        // the imageFormat param has priority here. But if the format is undefined, we relly on the alpha channel
        //是否是PNG类型的图片
        if (imageFormat != SDImageFormatUndefined) {
            usePNG = (imageFormat == SDImageFormatPNG);
        }
        //根据不同的图片类型获取到对应的图片data
        if (usePNG) {
            imageData = UIImagePNGRepresentation(self);
        } else {
            imageData = UIImageJPEGRepresentation(self, (CGFloat)1.0);
        }
#else
        NSBitmapImageFileType imageFileType = NSJPEGFileType;
        if (imageFormat == SDImageFormatGIF) {
            imageFileType = NSGIFFileType;
        } else if (imageFormat == SDImageFormatPNG) {
            imageFileType = NSPNGFileType;
        }
        
        imageData = [NSBitmapImageRep representationOfImageRepsInArray:self.representations
                                                             usingType:imageFileType
                                                            properties:@{}];
#endif
    }
    return imageData;
}

5 UIImage+GIF分类分析

UIImage+GIF实现了对GIF图片的NSData的处理。并且处理方法就是取出GIF图片的第一张UIImage来显示。如果真的要显示动态图片的话,我们需要使用FLAnimatedImageView来显示。

/**
 根据gif图片的data生成对应的gif的UIImage对象。而且只会取GIF图片的第一张UIImage。

 @param data gif图片的data对象
 @return 生成的image对象。这里只获取gif图片的第一张图像,如果要实现gif完整图像,使用FLAnimatedImageView
 */
+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
    if (!data) {
        return nil;
    }
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    //获取GIF图片包含的UIImage数量
    size_t count = CGImageSourceGetCount(source);
    UIImage *staticImage;
    //如果只有一张UIImage
    if (count <= 1) {
        staticImage = [[UIImage alloc] initWithData:data];
    } else {
#if SD_WATCH
        CGFloat scale = 1;
        scale = [WKInterfaceDevice currentDevice].screenScale;
#elif SD_UIKIT
        CGFloat scale = 1;
        scale = [UIScreen mainScreen].scale;
#endif
        //获取第一张UIImage对象
        CGImageRef CGImage = CGImageSourceCreateImageAtIndex(source, 0, NULL);
#if SD_UIKIT || SD_WATCH
        //获取gif图片的第一张图片
        UIImage *frameImage = [UIImage imageWithCGImage:CGImage scale:scale orientation:UIImageOrientationUp];
        //用第一张图片生成一个新的gif图片
        staticImage = [UIImage animatedImageWithImages:@[frameImage] duration:0.0f];
#elif SD_MAC
        staticImage = [[UIImage alloc] initWithCGImage:CGImage size:NSZeroSize];
#endif
        CGImageRelease(CGImage);
    }

    CFRelease(source);

    return staticImage;
}
/**
 判断一张图片是不是GIF图片

 @return bool值
 */
- (BOOL)isGIF {
    return (self.images != nil);
}

6 SDWebImageDecoder分析

通过这个类实现图片的解压缩操作。对于太大的图片,先按照一定比例缩小图片然后再解压缩。

#if SD_UIKIT || SD_WATCH
//每个像素占用的字节数
static const size_t kBytesPerPixel = 4;
//色彩空间占用的字节数
static const size_t kBitsPerComponent = 8;

/**
 解压缩图片

 @param image UIImage对象
 @return 返回解压缩以后的图片
 */
+ (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 colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];
        //宽度和高度
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        //图片占用的字节数
        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.
        //创建一个绘制图片的上下文
        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);
        //创建一个么有alpha通道的图片
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        //得到解压缩以后的图片
        UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
                                                         scale:image.scale
                                                   orientation:image.imageOrientation];
        
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        
        return imageWithoutAlpha;
    }
}

/*
 *定义一张图片可以占用的最大空间
 */
static const CGFloat kDestImageSizeMB = 60.0f;

static const CGFloat kSourceImageTileSizeMB = 20.0f;

static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
//1MB可以存储多少像素
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
//如果像素小于这个值,则不解压缩
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;

static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to overlap the seems where tiles meet.



/**
 如果原始图片占用的空间太大。则按照一定的比例解压缩。从而不让解压缩以后的图片占用的空间太大。

 @param image UIImage对象
 @return 返回处理结束的UIImage对象
 */
+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
    //图片是否支持解压缩
    if (![UIImage shouldDecodeImage:image]) {
        return image;
    }
    //图片不需要处理。直接解压缩
    if (![UIImage shouldScaleDownImage:image]) {
        return [UIImage decodedImageWithImage:image];
    }
    
    CGContextRef destContext;
    
    // 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 sourceImageRef = image.CGImage;
        
        CGSize sourceResolution = CGSizeZero;
        //获取原始图片的宽度和高度
        sourceResolution.width = CGImageGetWidth(sourceImageRef);
        sourceResolution.height = CGImageGetHeight(sourceImageRef);
        //获取原始图片的总像素
        float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
        // Determine the scale ratio to apply to the input image
        // that results in an output image of the defined size.
        // see kDestImageSizeMB, and how it relates to destTotalPixels.
        //根据一定的比例设置目标图片的宽度和高度
        float imageScale = kDestTotalPixels / sourceTotalPixels;
        CGSize destResolution = CGSizeZero;
        destResolution.width = (int)(sourceResolution.width*imageScale);
        destResolution.height = (int)(sourceResolution.height*imageScale);
        
        // current color space
        //获取原始图片的像素空间。默认是RGB
        CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:sourceImageRef];
        //每一行像素占用的内存空间大小
        size_t bytesPerRow = kBytesPerPixel * destResolution.width;
        
        // Allocate enough pixel data to hold the output image.
        //目标图片占用的总内存空间大小。一行占用内存空间大小*高度
        void* destBitmapData = malloc( bytesPerRow * destResolution.height );
        if (destBitmapData == NULL) {
            return image;
        }
        //根据各种设置创建一个上下文环境
        destContext = CGBitmapContextCreate(destBitmapData,
                                            destResolution.width,
                                            destResolution.height,
                                            kBitsPerComponent,
                                            bytesPerRow,
                                            colorspaceRef,
                                            kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
        
        if (destContext == NULL) {
            free(destBitmapData);
            return image;
        }
        //设置目标图片的质量
        CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
        CGRect sourceTile = CGRectZero;
        sourceTile.size.width = sourceResolution.width;
        sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
        sourceTile.origin.x = 0.0f;
        CGRect destTile;
        destTile.size.width = destResolution.width;
        destTile.size.height = sourceTile.size.height * imageScale;
        destTile.origin.x = 0.0f;
        float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
        CGImageRef sourceTileImageRef;
        int iterations = (int)( sourceResolution.height / sourceTile.size.height );
        int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
        if(remainder) {
            iterations++;
        }
        // Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.
        float sourceTileHeightMinusOverlap = sourceTile.size.height;
        sourceTile.size.height += sourceSeemOverlap;
        destTile.size.height += kDestSeemOverlap;
        for( int y = 0; y < iterations; ++y ) {
            @autoreleasepool {
                sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
                destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
                sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
                if( y == iterations - 1 && remainder ) {
                    float dify = destTile.size.height;
                    destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
                    dify -= destTile.size.height;
                    destTile.origin.y += dify;
                }
                CGContextDrawImage( destContext, destTile, sourceTileImageRef );
                CGImageRelease( sourceTileImageRef );
            }
        }
        
        CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
        CGContextRelease(destContext);
        if (destImageRef == NULL) {
            return image;
        }
        //生成处理结束以后的图片
        UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
        CGImageRelease(destImageRef);
        if (destImage == nil) {
            return image;
        }
        return destImage;
    }
}

/**
 imge是否能够加压缩

 @param image 图片
 @return 能否解压缩
 */
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
    // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
    if (image == nil) {
        return NO;
    }

    // do not decode animated images
    //如果是动态图片不处理
    if (image.images != nil) {
        return NO;
    }
    
    CGImageRef imageRef = image.CGImage;
    //获取image的alpha通道。通过通道获取图片数据
    CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
    BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
                     alpha == kCGImageAlphaLast ||
                     alpha == kCGImageAlphaPremultipliedFirst ||
                     alpha == kCGImageAlphaPremultipliedLast);
    // do not decode images with alpha
    //如果有alpha通道值,则不处理
    if (anyAlpha) {
        return NO;
    }
    
    return YES;
}

/**
是否需要减少原始图片的大小

 @param image UIImage对象
 @return 是否支持scale
 */
+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image {
    BOOL shouldScaleDown = YES;
        
    CGImageRef sourceImageRef = image.CGImage;
    CGSize sourceResolution = CGSizeZero;
    sourceResolution.width = CGImageGetWidth(sourceImageRef);
    sourceResolution.height = CGImageGetHeight(sourceImageRef);
    //图片总共像素
    float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
    //如果图片的总像素大于一定比例,则需要做简化处理
    float imageScale = kDestTotalPixels / sourceTotalPixels;
    if (imageScale < 1) {
        shouldScaleDown = YES;
    } else {
        shouldScaleDown = NO;
    }
    
    return shouldScaleDown;
}

/**
 获取图片的色彩空间

 @param imageRef 图片
 @return 色彩空间
 */
+ (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef {
    // current
    CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
    CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
    
    BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
                                  imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
                                  imageColorSpaceModel == kCGColorSpaceModelCMYK ||
                                  imageColorSpaceModel == kCGColorSpaceModelIndexed);
    if (unsupportedColorSpace) {
        colorspaceRef = CGColorSpaceCreateDeviceRGB();
        CFAutorelease(colorspaceRef);
    }
    return colorspaceRef;
}
#elif SD_MAC
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
    return image;
}

+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
    return image;
}

7 总结

下面是几个分类工具的使用。

/**
 根据图片数据获取图片类型

 */
- (IBAction)getImageType:(id)sender {
    NSData *imageData = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"rock.gif" ofType:nil]];
    SDImageFormat formate = [NSData sd_imageFormatForImageData:imageData];
    NSString *message = [NSString stringWithFormat:@"%d",formate];
    showMessage(message,self);
}


/**
 获取一张图片对应的两倍或者三倍屏幕对应的图片

 */
- (IBAction)getScaleImage:(id)sender {
    UIImage *sourceImage = [UIImage imageNamed:@"2.png"];
    UIImage *dis2ScaleImage = SDScaledImageForKey(@"dist@2x.png", sourceImage);
    UIImage *dis3ScaleImage = SDScaledImageForKey(@"dist@3x.png", sourceImage);
    NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    //NSLog(@"document:%@",documentPath);
    NSString *path1 = [documentPath stringByAppendingPathComponent:@"dist.png"];
    [UIImagePNGRepresentation(sourceImage) writeToFile:path1 atomically:YES];
    NSString *path2 = [documentPath stringByAppendingPathComponent:@"dist@2x.png"];
    [UIImagePNGRepresentation(dis2ScaleImage) writeToFile:path2 atomically:YES];
    NSString *path3 = [documentPath stringByAppendingPathComponent:@"dist@3x.png"];
    [UIImagePNGRepresentation(dis3ScaleImage) writeToFile:path3 atomically:YES];
}

/**
 解压缩图片

 @param sender 解压缩图片
 */
- (IBAction)unZipImage:(id)sender {
    UIImage *sourceImage = [UIImage imageNamed:@"2.png"];
    UIImage *distImage = [UIImage decodedAndScaledDownImageWithImage:sourceImage];
    NSString *documentPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSString *path1 = [documentPath stringByAppendingPathComponent:@"distImage.png"];
    [UIImagePNGRepresentation(distImage) writeToFile:path1 atomically:YES];
    NSString *path2 = [documentPath stringByAppendingPathComponent:@"sourceImage.png"];
    [UIImagePNGRepresentation(sourceImage) writeToFile:path2 atomically:YES];
    
}

最后原文地址.html),demo地址

查看原文

赞 0 收藏 2 评论 0

huang303513 发布了文章 · 2017-05-03

SDWebImage源码解析(三)

1 概述

这篇博文中,我将分析SDWebImageManagerSDImageCacheSDWebImageManager拥有一个SDWebImageCacheSDWebImageDownloader属性分别用于图片的缓存和加载处理。为UIView及其子类提供了加载图片的统一接口。管理正在加载操作的集合,这个类是一个单列。同时管理各种加载选项的处理。SDImageCache负责SDWebImage的整个缓存工作,是一个单列对象。缓存路径处理、缓存名字处理、管理内存缓存和磁盘缓存的创建和删除、根据指定key获取图片、存入图片的类型处理、根据缓存的创建和修改日期删除缓存。

2 SDWebImageManager分析

UIImageView等各种视图通过UIView+WebCache分类的sd_internalSetImageWithURL方法来调用SDWebImageManager类的如下方法实现图片加载:

/**
 这个方法是核心方法。UIImageView等这种分类都默认通过调用这个方法来获取数据。

 @param url 图片的url地址
 @param options 获取图片的属性
 @param progressBlock 加载进度回调
 @param completedBlock 加载完成回调
 @return 返回一个加载的载体对象。以便提供给后面取消删除等。
 */
- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                              options:(SDWebImageOptions)options
                                             progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                            completed:(nullable SDInternalCompletionBlock)completedBlock;

首先看他地第二个参数options。这个参数指定了图片加载过程中的不同选项。指定不同选项,SDWebImage可以根据选项做不同的处理,这是一个枚举类型,多个选项之间可以组合使用。

/**
 枚举,定义了图片加载处理过程中的选项
 */
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    /*
     默认情况下,当一个URL下载失败的时候,这个URL会被加入黑名单列表,下次再有这个url的请求则停止请求。
     如果为true,这个值表示需要再尝试请求。
     */
    SDWebImageRetryFailed = 1 << 0,

    /*
     默认情况下,当UI可以交互的时候就开始加载图片。这个标记可以阻止这个时候加载。
     而是当UIScrollView开始减速滑动的时候开始加载。
     */
    SDWebImageLowPriority = 1 << 1,

    /*
     这个属性禁止磁盘缓存
     */
    SDWebImageCacheMemoryOnly = 1 << 2,

    /*
     这个标记允许图片在加载过程中显示,就像浏览器那样。
     默认情况下,图片只会在加载完成以后再显示。
     */
    SDWebImageProgressiveDownload = 1 << 3,

    /*
     *即使本地已经缓存了图片,但是根据HTTP的缓存策略去网络上加载图片。也就是说本地缓存了也不管了,尝试从网络上加载数据。但是具体是从代理加载、HTTP缓存加载、还是原始服务器加载这个就更具HTTP的请求头配置。
     *使用NSURLCache而不是SDWebImage来处理磁盘缓存。从而可能会导致轻微的性能损害。
     *这个选项专门用于处理,url地址没有变,但是url对于的图片数据在服务器改变的情况。
     *如果一个缓存图片更新了,则completion这个回调会被调用两次,一次返回缓存图片,一次返回最终图片。
     *我们只有在不能确保URL和他对应的内容不能完全对应的时候才使用这个标记。
     */
    SDWebImageRefreshCached = 1 << 4,

    /*
     当应用进入后台以后,图片继续下载。应用进入后台以后,通过向系统申请额外的时间来完成。如果时间超时,那么下载操作会被取消。
     */
    SDWebImageContinueInBackground = 1 << 5,

    /*
     处理缓存在`NSHTTPCookieStore`对象里面的cookie。通过设置`NSMutableURLRequest.HTTPShouldHandleCookies = YES`来实现的。
     */
    SDWebImageHandleCookies = 1 << 6,

    /*
     *允许非信任的SSL证书请求。
     *在测试的时候很有用。但是正式环境要小心使用。
     */
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,

    /*
     * 默认情况下,图片加载的顺序是根据加入队列的顺序加载的。但是这个标记会把任务加入队列的最前面。
     */
    SDWebImageHighPriority = 1 << 8,
    
    /*
     默认情况下,在图片加载的过程中,会显示占位图。
     但是这个标记会阻止显示占位图直到图片加载完成。
     */
    SDWebImageDelayPlaceholder = 1 << 9,

    /*
     *默认情况下,我们不会去调用`animated images`(估计就是多张图片循环显示或者GIF图片)的`transformDownloadedImage`代理方法来处理图片。因为大部分transformation操作会对图片做无用处理。
     *用这个标记表示无论如何都要对图片做transform处理。
     */
    SDWebImageTransformAnimatedImage = 1 << 10,
    
    /*
     *默认情况下,图片再下载完成以后都会被自动加载到UIImageView对象上面。但是有时我们希望UIImageView加载我们手动处理以后的图片。
     *这个标记允许我们在completion这个Block中手动设置处理好以后的图片。
     */
    SDWebImageAvoidAutoSetImage = 1 << 11,
    
    /*
     *默认情况下,图片会按照他的原始大小来解码显示。根据设备的内存限制,这个属性会调整图片的尺寸到合适的大小再解压缩。
     *如果`SDWebImageProgressiveDownload`标记被设置了,则这个flag不起作用。
     */
    SDWebImageScaleDownLargeImages = 1 << 12
};

接下来我先看SDWebImageManager的初始化过程。

- (nonnull instancetype)init {
    SDImageCache *cache = [SDImageCache sharedImageCache];
    SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
    return [self initWithCache:cache downloader:downloader];
}

/**
 初始化SDImageCache和SDWebImageDownloader对象

 @param cache SDImageCache对象
 @param downloader SDWebImageDownloader对象
 @return 返回初始化结果
 */
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
    if ((self = [super init])) {
        _imageCache = cache;
        _imageDownloader = downloader;
        //用于保存加载失败的url集合
        _failedURLs = [NSMutableSet new];
        //用于保存当前正在加载的Operation
        _runningOperations = [NSMutableArray new];
    }
    return self;
}

loadImageWithURL方法是SDWebImageManager最核心的方法,实现过程:

/**
 这个方法是核心方法。UIImageView等这种分类都默认通过调用这个方法来获取数据。

 @param url 图片的url地址
 @param options 获取图片的属性
 @param progressBlock 加载进度回调
 @param completedBlock 加载完成回调
 @return 返回一个加载的载体对象。以便提供给后面取消删除等。
 */
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock {
    /*
     如果传入的url是NSString格式的。则转换为NSURL类型再处理
     */
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    //如果url不会NSURL类型的对象。则置为nil
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
    /*
     图片加载获取获取过程中绑定一个`SDWebImageCombinedOperation`对象。以方便后续再通过这个对象对url的加载控制。
     */
    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    BOOL isFailedUrl = NO;
    //当前url是否在失败url的集合里面
    if (url) {
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    }
    /*
     如果url是失败的url或者url有问题等各种问题。则直接根据opeation来做异常情况的处理
     */
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        //构建回调Block
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
        return operation;
    }
    //把加载图片的一个载体存入runningOperations。里面是所有正在做图片加载过程的operation的集合。
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    //根据url获取url对应的key
    NSString *key = [self cacheKeyForURL:url];
    /*
    *如果图片是从内存加载,则返回的cacheOperation是nil,
    *如果是从磁盘加载,则返回的cacheOperation是`NSOperation`对象。
    *如果是从网络加载,则返回的cacheOperation对象是`SDWebImageDownloaderOperation`对象。
    */
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        //从缓存中获取图片数据返回
        //如果已经取消了操作。则直接返回并且移除对应的opetation对象
        if (operation.isCancelled) {
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }
        
        if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            /**
             如果从缓存获取图片失败。或者设置了SDWebImageRefreshCached来忽略缓存。则先把缓存的图片返回。
             */
            if (cachedImage && options & SDWebImageRefreshCached) {
                //构建回调Block
                [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }

            // download if no image or requested to refresh anyway, and download allowed by delegate
            /*
             把图片加载的`SDWebImageOptions`类型枚举转换为图片下载的`SDWebImageDownloaderOptions`类型的枚举
             */
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
            if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
            if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
            if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
            if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
            if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
            /*
             如果设置了强制刷新缓存的选项。则`SDWebImageDownloaderProgressiveDownload`选项失效并且添加`SDWebImageDownloaderIgnoreCachedResponse`选项。
             */
            if (cachedImage && options & SDWebImageRefreshCached) {
                // force progressive off if image already cached but forced refreshing
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // ignore image read from NSURLCache if image if cached but force refreshing
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            /*
             新建一个网络下载的操作。
             */
            SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                //如果图片下载结束以后,对应的图片加载操作已经取消。则什么处理都不做
                if (!strongOperation || strongOperation.isCancelled) {
                   //如果operation已经被取消了,则什么也不做
                } else if (error) {
                    //如果加载出错。则直接返回回调。并且添加到failedURLs中
                    [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];

                    if (   error.code != NSURLErrorNotConnectedToInternet
                        && error.code != NSURLErrorCancelled
                        && error.code != NSURLErrorTimedOut
                        && error.code != NSURLErrorInternationalRoamingOff
                        && error.code != NSURLErrorDataNotAllowed
                        && error.code != NSURLErrorCannotFindHost
                        && error.code != NSURLErrorCannotConnectToHost
                        && error.code != NSURLErrorNetworkConnectionLost) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {
                    //网络图片加载成功
                    if ((options & SDWebImageRetryFailed)) {
                        //如果有重试失败下载的选项。则把url从failedURLS中移除
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                        //如果成功下载图片。并且图片是动态图片。并且设置了SDWebImageTransformAnimatedImage属性。则处理图片
                    } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            //获取transform以后的图片
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
                            //存储transform以后的的图片
                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                // pass nil if the image was transformed, so we can recalculate the data from the image
                                [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            //回调拼接
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {
                        //如果成功下载图片。并且图片不是图片。则直接缓存和回调
                        if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                        }
                        //回调拼接
                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }
                //从正在加载的图片操作集合中移除当前操作
                if (finished) {
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];
            //重置cancelBlock,取消下载operation
            operation.cancelBlock = ^{
                [self.imageDownloader cancel:subOperationToken];
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                [self safelyRemoveOperationFromRunning:strongOperation];
            };
        } else if (cachedImage) {
            //如果获取到了缓存图片。在直接通过缓存图片处理
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        } else {
            // Image not in cache and download disallowed by delegate
            //图片么有缓存、并且图片也没有下载
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }
    }];

    return operation;
}

通过对这个方法的分析,他主要实现的功能有:

  • 创建一个SDWebImageCombinedOperation对象,调用者可以通过这个对象来对加载做取消等操作。这个对象的cancelOperation属性有如下几种情况。

    • 如果图片是从内存加载,则返回的cacheOperation是nil。

    • 如果是从磁盘加载,则返回的cacheOperation是NSOperation对象。

    • 如果是从网络加载,则返回的cacheOperation对象是SDWebImageDownloaderOperation对象。

  • 通过failedURLs属性来保存加载失败的url。通过它可以阻止失败的url再次加载,提高用户体验。

  • 通过runningOperations属性记录当前正在加载的Operation列表。

  • 加载结束以后,通过callCompletionBlockForOperation方法来拼接回调Block。

  • SDWebImageOptions类型的枚举值转换为SDWebImageDownloaderOptions类型的枚举值。

  • 图片成功从网络加载以后,通过imageCache属性的storeImage方法来缓存图片。

另外还有一些辅助性的方法,用于处理缓存判断、url与key转换等功能:

/**
 根据url获取url对应的缓存key。
 如果有实现指定的url转换key的Block,则用这个方式转换为key。
 否则直接用url的绝对值多为key

 @param url url
 @return 缓存的key
 */
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
    if (!url) {
        return @"";
    }
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    } else {
        return url.absoluteString;
    }
}

/**
 一个url的缓存是否存在

 @param url 缓存数据对应的url
 @param completionBlock 缓存结果回调
 */
- (void)cachedImageExistsForURL:(nullable NSURL *)url
                     completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {
    NSString *key = [self cacheKeyForURL:url];
    //内存里面是否有key的缓存
    BOOL isInMemoryCache = ([self.imageCache imageFromMemoryCacheForKey:key] != nil);
    //内存缓存
    if (isInMemoryCache) {
        // making sure we call the completion block on the main queue
        dispatch_async(dispatch_get_main_queue(), ^{
            if (completionBlock) {
                completionBlock(YES);
            }
        });
        return;
    }
    //磁盘缓存
    [self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
        // the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
        if (completionBlock) {
            completionBlock(isInDiskCache);
        }
    }];
}

/**
 url是否有磁盘缓存数据

 @param url url
 @param completionBlock 回调
 */
- (void)diskImageExistsForURL:(nullable NSURL *)url
                   completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {
    NSString *key = [self cacheKeyForURL:url];
    
    [self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
        // the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
        if (completionBlock) {
            completionBlock(isInDiskCache);
        }
    }];
}

2.1 SDWebImageCombinedOperation分析

从上面的方法,发现loadImageWithURL方法会返回一个SDWebImageCombinedOperation给调用者,这个对象有个cancel方法,这个方法继承自SDWebImageOperation协议。方法里面会调用SDWebImageDownlaoderOperation或者NSOperation的cancel方法

/**
 通过这个对象关联一个`SDWebImageDownloaderOperation`对象
 */
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>

/**
 用于判断Operation是否已经取消
 */
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;

/**
 取消回调
 */
@property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock;

/**
 SDWebImageDownloaderOperation对象。可以通过这个属性取消一个NSOperation
 */
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;

@end

@implementation SDWebImageCombinedOperation

/**
 取消Operation的回调Block

 @param cancelBlock 回调Block
 */
- (void)setCancelBlock:(nullable SDWebImageNoParamsBlock)cancelBlock {
    // check if the operation is already cancelled, then we just call the cancelBlock
    if (self.isCancelled) {
        if (cancelBlock) {
            cancelBlock();
        }
        _cancelBlock = nil; // don't forget to nil the cancelBlock, otherwise we will get crashes
    } else {
        _cancelBlock = [cancelBlock copy];
    }
}

/**
 调用cancel方法。这个方法继承自`SDWebImageOperation`协议。方法里面会调用`SDWebImageDownlaoderOperation`或者`NSOperation`的cancel方法
 */
- (void)cancel {
    self.cancelled = YES;
    if (self.cacheOperation) {
        //调用`SDWebImageDownlaoderOperation`或者`NSOperation`的cancel方法
        [self.cacheOperation cancel];
        self.cacheOperation = nil;
    }
    if (self.cancelBlock) {
        self.cancelBlock();
        
        // TODO: this is a temporary fix to #809.
        // Until we can figure the exact cause of the crash, going with the ivar instead of the setter
        //        self.cancelBlock = nil;
        _cancelBlock = nil;
    }
}

@end

那么取消一个Operation是在哪里呢?其实就在UIView+WebCache中,所有使用SDWebImage的View都可以使用这个方法来取消下载Operation:

/**
 取消当前Class对应的所有加载请求
 */
- (void)sd_cancelCurrentImageLoad {
    [self sd_cancelImageLoadOperationWithKey:NSStringFromClass([self class])];
}

上面这个方法又会调用UIView+WebCacheOperation分类的sd_cancelImageLoadOperationWithKey方法来实现,这个方法会调用SDWebImageCombinedOperation对象的cancel方法,然后在cancel方法中再调用SDWebImageDownloadOperation或者NSOperationcancel方法:

/**
 取消当前key对应的所有`SDWebImageCombinedOperation`对象

 @param key Operation对应的key
 */
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    // Cancel in progress downloader from queue
    //获取当前View对应的所有key
    SDOperationsDictionary *operationDictionary = [self operationDictionary];
    //获取对应的图片加载Operation
    id operations = operationDictionary[key];
    //取消所有当前View对应的所有Operation
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id <SDWebImageOperation> operation in operations) {
                if (operation) {
                    //SDWebImageCombinedOperation对象的cancel方法
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id<SDWebImageOperation>) operations cancel];
        }
        [operationDictionary removeObjectForKey:key];
    }
}

3 SDImageCache分析

从上面的代码中,发现SDWebImageManager通过SDImageCache来获取/存储图片,而且SDImageCache是一个单列对象。他的具体实现可以总结为如下几点:

  • 通过AutoPurgeCache这个NSCache子类来管理内存缓存。当接收到内存警告的时候,移除内存缓存的所有对象。

  • 接收到UIApplicationDidReceiveMemoryWarningNotification通知以后,会删除内存中缓存的图片。

  • 接收到UIApplicationWillTerminateNotification通知以后,会通过deleteOldFiles方法删除老的图片。具体删除规则如下:

    • 缓存大小、过期日期、是否解压缩缓存、是否允许内存缓存都是通过SDImageCacheConfig这个对象来配置的。

    • 首先会迭代缓存目录下的所有文件,对于大于一周的图片数据全部删除。

    • 然后会记录缓存目录的所有大小,如果当前缓存大于默认缓存,则按照创建日期开始删除图片缓存,直到缓存大小小于默认缓存大小。

  • 当接收到UIApplicationDidEnterBackgroundNotification通知以后,会调用deleteOldFilesWithCompletionBlock来清理缓存数据。

  • 定义了一些列方法来处理图片的获取、缓存、移除操作。主要有下面几个方法:

    • queryCacheOperationForKey查询指定key对应的缓存图片,先从内存查找,然后从磁盘查找。

    • removeImageForKey移除指定的缓存图片。

    • diskImageDataBySearchingAllPathsForKey在磁盘上查找指定key对应的图片。

    • storeImageDataToDisk把指定的图片数据存入磁盘。

  • 通过cachedFileNameForKey方法获取一张图片对应的MD5加密的缓存名字。

SDImageCache的完整实现如下:

@interface AutoPurgeCache : NSCache
@end

/**
 如果接收到内存警告、移除所有的缓存对象
 */
@implementation AutoPurgeCache

- (nonnull instancetype)init {
    self = [super init];
    if (self) {
#if SD_UIKIT
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
    }
    return self;
}

- (void)dealloc {
#if SD_UIKIT
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}

@end
/**
 计算一张图片占用内存的大小

 @param image 图片
 @return 占用内存大小
 */
FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
#if SD_MAC
    return image.size.height * image.size.width;
#elif SD_UIKIT || SD_WATCH
    return image.size.height * image.size.width * image.scale * image.scale;
#endif
}
/**
 单列对象、用处管理图片的缓存以及缓存图片的处理
 */
@interface SDImageCache ()

#pragma mark - Properties
@property (strong, nonatomic, nonnull) NSCache *memCache;
@property (strong, nonatomic, nonnull) NSString *diskCachePath;
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;

@end

@implementation SDImageCache {
    NSFileManager *_fileManager;
}
#pragma mark - Singleton, init, dealloc
+ (nonnull instancetype)sharedImageCache {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}

- (instancetype)init {
    return [self initWithNamespace:@"default"];
}

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns {
    NSString *path = [self makeDiskCachePath:ns];
    return [self initWithNamespace:ns diskCacheDirectory:path];
}

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory {
    if ((self = [super init])) {
        NSString *fullNamespace = [@"com.hackemist.SDWebImageCache." stringByAppendingString:ns];
        // 初始化一个串行的dispatch_queue_t
        _ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
        //初始化缓存策略配置对象
        _config = [[SDImageCacheConfig alloc] init];
        
        // 初始化内存缓存对象
        _memCache = [[AutoPurgeCache alloc] init];
        _memCache.name = fullNamespace;

        // 初始化磁盘缓存路径
        if (directory != nil) {
            _diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
        } else {
            NSString *path = [self makeDiskCachePath:ns];
            _diskCachePath = path;
        }

        dispatch_sync(_ioQueue, ^{
            _fileManager = [NSFileManager new];
        });

#if SD_UIKIT
        /*
         当应用收到内存警告的时候,清除内存缓存。
         */
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(clearMemory)
                                                     name:UIApplicationDidReceiveMemoryWarningNotification
                                                   object:nil];
        /*
         当应用终止的时候,清除老数据
         */
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(deleteOldFiles)
                                                     name:UIApplicationWillTerminateNotification
                                                   object:nil];
        /*
         当应用进入后台的时候,在后台删除老数据
         */
        [[NSNotificationCenter defaultCenter] addObserver:self
                                                 selector:@selector(backgroundDeleteOldFiles)
                                                     name:UIApplicationDidEnterBackgroundNotification
                                                   object:nil];
#endif
    }

    return self;
}

- (void)dealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    SDDispatchQueueRelease(_ioQueue);
}

/**
 当前queue是否在ioQueue
 */
- (void)checkIfQueueIsIOQueue {
    const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
    const char *ioQueueLabel = dispatch_queue_get_label(self.ioQueue);
    if (strcmp(currentQueueLabel, ioQueueLabel) != 0) {
        NSLog(@"This method should be called from the ioQueue");
    }
}

#pragma mark - Cache paths
/*
 添加只能读的缓存目录
 */
- (void)addReadOnlyCachePath:(nonnull NSString *)path {
    if (!self.customPaths) {
        self.customPaths = [NSMutableArray new];
    }

    if (![self.customPaths containsObject:path]) {
        [self.customPaths addObject:path];
    }
}

/**
 获取指定key对应的完整缓存路径

 @param key key,对应一张图片。比如图片的名字
 @param path 指定根目录
 @return 完整目录
 */
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
    NSString *filename = [self cachedFileNameForKey:key];
    return [path stringByAppendingPathComponent:filename];
}

- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key {
    return [self cachePathForKey:key inPath:self.diskCachePath];
}


/**
 MD5加密
 X 表示以十六进制形式输出
 02 表示不足两位,前面补0输出;出过两位,不影响

 @param key key
 @return 加密后的数据
 */
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
    const char *str = key.UTF8String;
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]];

    return filename;
}

/**
 图片缓存目录

 @param fullNamespace 自定义目录
 @return 完整目录
 */
- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace {
    NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
    return [paths[0] stringByAppendingPathComponent:fullNamespace];
}

#pragma mark - 图片缓存具体实现的一些列方法

- (void)storeImage:(nullable UIImage *)image
            forKey:(nullable NSString *)key
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    [self storeImage:image imageData:nil forKey:key toDisk:YES completion:completionBlock];
}

- (void)storeImage:(nullable UIImage *)image
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    [self storeImage:image imageData:nil forKey:key toDisk:toDisk completion:completionBlock];
}

/**
 把一张图片存入缓存的具体实现

 @param image 缓存的图片对象
 @param imageData 缓存的图片数据
 @param key 缓存对应的key
 @param toDisk 是否缓存到瓷片
 @param completionBlock 缓存完成回调
 */
- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    if (!image || !key) {
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    //缓存到内存
    if (self.config.shouldCacheImagesInMemory) {
        //计算缓存数据的大小
        NSUInteger cost = SDCacheCostForImage(image);
        //加入缓存对此昂
        [self.memCache setObject:image forKey:key cost:cost];
    }
    
    if (toDisk) {
        /*
         在一个线性队列中做磁盘缓存操作。
         */
        dispatch_async(self.ioQueue, ^{
            NSData *data = imageData;
            if (!data && image) {
                //获取图片的类型GIF/PNG等
                SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];
                //根据指定的SDImageFormat。把图片转换为对应的data数据
                data = [image sd_imageDataAsFormat:imageFormatFromData];
            }
            //把处理好了的数据存入磁盘
            [self storeImageDataToDisk:data forKey:key];
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    } else {
        if (completionBlock) {
            completionBlock();
        }
    }
}

/**
 把图片资源存入磁盘

 @param imageData 图片数据
 @param key key
 */
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
    if (!imageData || !key) {
        return;
    }
    
    [self checkIfQueueIsIOQueue];
    //缓存目录是否已经初始化
    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    
    // get cache Path for image key
    //获取key对应的完整缓存路径
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    // transform to NSUrl
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    //把数据存入路径
    [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
    
    // disable iCloud backup
    if (self.config.shouldDisableiCloud) {
        //给文件添加到运行存储到iCloud属性
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

#pragma mark - Query and Retrieve Ops

- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {
    dispatch_async(_ioQueue, ^{
        BOOL exists = [_fileManager fileExistsAtPath:[self defaultCachePathForKey:key]];

        // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
        // checking the key with and without the extension
        if (!exists) {
            exists = [_fileManager fileExistsAtPath:[self defaultCachePathForKey:key].stringByDeletingPathExtension];
        }

        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock(exists);
            });
        }
    });
}

/**
 根据key获取缓存在内存中的图片

 @param key key
 @return 缓存的图片
 */
- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
    return [self.memCache objectForKey:key];
}

- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key {
    UIImage *diskImage = [self diskImageForKey:key];
    if (diskImage && self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(diskImage);
        [self.memCache setObject:diskImage forKey:key cost:cost];
    }

    return diskImage;
}

- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key {
    // First check the in-memory cache...
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        return image;
    }
    
    // Second check the disk cache...
    image = [self imageFromDiskCacheForKey:key];
    return image;
}

/**
 根据指定的key,获取存储在磁盘上的数据

 @param key 图片对应的key
 @return 返回图片数据
 */
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
    //获取key对应的path
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath];
    if (data) {
        return data;
    }

    // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
    // checking the key with and without the extension
    /*
     如果key么有后缀名,则会走到这里通过这里读取
     */
    data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension];
    if (data) {
        return data;
    }
    /*
     如果在默认路径没有找到图片,则在自定义路径迭代查找
     */
    NSArray<NSString *> *customPaths = [self.customPaths copy];
    for (NSString *path in customPaths) {
        NSString *filePath = [self cachePathForKey:key inPath:path];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath];
        if (imageData) {
            return imageData;
        }

        // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
        // checking the key with and without the extension
        imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension];
        if (imageData) {
            return imageData;
        }
    }

    return nil;
}

/**
 根据指定的key获取image对象

 @param key key
 @return image对象
 */
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
    //获取磁盘数据
    NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
    if (data) {
        UIImage *image = [UIImage sd_imageWithData:data];
        image = [self scaledImageForKey:key image:image];
        if (self.config.shouldDecompressImages) {
            image = [UIImage decodedImageWithImage:image];
        }
        return image;
    }
    else {
        return nil;
    }
}

- (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
    return SDScaledImageForKey(key, image);
}

/**
 在缓存中查询对应key的数据。通过一个NSOperation来完成

 @param key 要查询的key
 @param doneBlock 查询结束以后的Block
 @return 返回做查询操作的Block
 */
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    // First check the in-memory cache...
    //首先从内测中查找图片
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        NSData *diskData = nil;
        //是否是gif图片
        if ([image isGIF]) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
        return nil;
    }
    //新建一个NSOperation来获取磁盘图片
    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }
        //在一个自动释放池中处理图片从磁盘加载
        @autoreleasepool {
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                //把从磁盘取出的缓存图片加入内存缓存中
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            //图片处理完成以后回调Block
            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });
    return operation;
}

#pragma mark - Remove Ops

- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion {
    [self removeImageForKey:key fromDisk:YES withCompletion:completion];
}

/**
 移除指定key对应的缓存数据

 @param key key
 @param fromDisk 是否也清除磁盘缓存
 @param completion 回调
 */
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion {
    if (key == nil) {
        return;
    }
    //移除内存缓存
    if (self.config.shouldCacheImagesInMemory) {
        [self.memCache removeObjectForKey:key];
    }
    //移除磁盘缓存
    if (fromDisk) {
        dispatch_async(self.ioQueue, ^{
            [_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
            
            if (completion) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completion();
                });
            }
        });
    } else if (completion){
        completion();
    }
    
}

# pragma mark - Mem Cache settings

- (void)setMaxMemoryCost:(NSUInteger)maxMemoryCost {
    self.memCache.totalCostLimit = maxMemoryCost;
}

- (NSUInteger)maxMemoryCost {
    return self.memCache.totalCostLimit;
}

- (NSUInteger)maxMemoryCountLimit {
    return self.memCache.countLimit;
}

- (void)setMaxMemoryCountLimit:(NSUInteger)maxCountLimit {
    self.memCache.countLimit = maxCountLimit;
}

#pragma mark - 内存缓存清理相关操作

/**
 清理当前SDImageCache对象的内存缓存
 */
- (void)clearMemory {
    [self.memCache removeAllObjects];
}

/**
 移除所有的缓存图片数据

 @param completion 移除完成以后回调
 */
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion {
    dispatch_async(self.ioQueue, ^{
        [_fileManager removeItemAtPath:self.diskCachePath error:nil];
        [_fileManager createDirectoryAtPath:self.diskCachePath
                withIntermediateDirectories:YES
                                 attributes:nil
                                      error:NULL];
        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion();
            });
        }
    });
}

- (void)deleteOldFiles {
    [self deleteOldFilesWithCompletionBlock:nil];
}

/**
 当应用终止或者进入后台都回调用这个方法来清除缓存图片。
 这里会根据图片存储时间来清理图片、默认是一周,从最老的图片开始清理。如果图片缓存空间小于一个规定值,则不考虑。

 @param completionBlock 清除完成以后的回调
 */
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        //获取磁盘缓存的默认根目录
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        
        NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

        // This enumerator prefetches useful properties for our cache files.
        /*
         第二个参数制定了需要获取的属性集合
         第三个参数表示不迭代隐藏文件
        */
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
        NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;

        // Enumerate all of the files in the cache directory.  This loop has two purposes:
        //
        //  1. Removing files that are older than the expiration date.
        //  2. Storing file attributes for the size-based cleanup pass.
        /*
         迭代缓存目录。有两个目的:
         1 删除比指定日期更老的图片
         2 记录文件的大小,以提供给后面删除使用
         */
        NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSError *error;
            //获取指定url对应文件的指定三种属性的key和value
            NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
            // Skip directories and errors.
            //如果是文件夹则返回
            if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            // Remove files that are older than the expiration date;
            //获取指定url文件对应的修改日期
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            //如果修改日期大于指定日期,则加入要移除的数组里
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }

            // Store a reference to this file and account for its total size.
            //获取指定的url对应的文件的大小,并且把url与对应大小存入一个字典中
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
            cacheFiles[fileURL] = resourceValues;
        }
        //删除所有最后修改日期大于指定日期的所有文件
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }

        // If our remaining disk cache exceeds a configured maximum size, perform a second
        // size-based cleanup pass.  We delete the oldest files first.
        /*
         如果我们当前缓存的大小超过了默认大小,则按照日期删除,直到缓存大小<默认大小的一半
         */
        if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
            // Target half of our maximum cache size for this cleanup pass.
            const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;

            // Sort the remaining cache files by their last modification time (oldest first).
            //根据文件创建的时间排序
            NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                     usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                         return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                     }];

            // Delete files until we fall below our desired cache size.
            /*
             迭代删除缓存,直到缓存大小是默认缓存大小的一半
             */
            for (NSURL *fileURL in sortedFiles) {
                if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    //总得缓存大小减去当前要删除文件的大小
                    currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;

                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        //执行完毕,主线程回调
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

#if SD_UIKIT

/**
 应用进入后台的时候,调用这个方法
 */
- (void)backgroundDeleteOldFiles {
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)];
    //如果backgroundTask对应的时间结束了。任务还么有处理完成。则直接终止任务
    __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
        // Clean up any unfinished task business by marking where you
        // stopped or ending the task outright.
        //当任务非正常终止的时候,做清理工作
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];

    // Start the long-running task and return immediately.
    //图片清理结束以后。处理完成
    [self deleteOldFilesWithCompletionBlock:^{
        //清理完成以后,终止任务
        [application endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
    }];
}
#endif

#pragma mark - Cache Info

- (NSUInteger)getSize {
    __block NSUInteger size = 0;
    dispatch_sync(self.ioQueue, ^{
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
        for (NSString *fileName in fileEnumerator) {
            NSString *filePath = [self.diskCachePath stringByAppendingPathComponent:fileName];
            NSDictionary<NSString *, id> *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:nil];
            size += [attrs fileSize];
        }
    });
    return size;
}

- (NSUInteger)getDiskCount {
    __block NSUInteger count = 0;
    dispatch_sync(self.ioQueue, ^{
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtPath:self.diskCachePath];
        count = fileEnumerator.allObjects.count;
    });
    return count;
}

- (void)calculateSizeWithCompletionBlock:(nullable SDWebImageCalculateSizeBlock)completionBlock {
    NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];

    dispatch_async(self.ioQueue, ^{
        NSUInteger fileCount = 0;
        NSUInteger totalSize = 0;

        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:@[NSFileSize]
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];

        for (NSURL *fileURL in fileEnumerator) {
            NSNumber *fileSize;
            [fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL];
            totalSize += fileSize.unsignedIntegerValue;
            fileCount += 1;
        }

        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock(fileCount, totalSize);
            });
        }
    });
}
@end

最后原文地址.html),demo地址

查看原文

赞 0 收藏 1 评论 1

huang303513 发布了文章 · 2017-05-03

SDWebImage源码解析(四)

1 概述

这篇博文将分析SDWebImageDownloaderSDWebImageDownloaderOperationSDWebImage通过这两个类处理图片的网络加载。SDWebImageManager通过属性imageDownloader持有SDWebImageDownloader并且调用它的downloadImageWithURL来从网络加载图片。SDWebImageDownloader实现了图片加载的具体处理,如果图片在缓存存在则从缓存区,如果缓存不存在,则直接创建一个SDWebImageDownloaderOperation对象来下载图片。管理NSURLRequest对象请求头的封装、缓存、cookie的设置。加载选项的处理等功能。管理Operation之间的依赖关系。SDWebImageDownloaderOperation是一个自定义的并行Operation子类。这个类主要实现了图片下载的具体操作、以及图片下载完成以后的图片解压缩、Operation生命周期管理等。

2 SDWebImageDownloader分析

SDWebImageDownlaoder是一个单列对象,主要做了如下工作:

  • 定义了SDWebImageDownloaderOptions这个枚举属性,通过这个枚举属性来设置图片从网络加载的不同情况。

  • 定义并管理了NSURLSession对象,通过这个对象来做网络请求,并且实现对象的代理方法。

  • 定义一个NSURLRequest对象,并且管理请求头的拼装。

  • 对于每一个网络请求,通过一个SDWebImageDownloaderOperation自定义的NSOperation来操作网络下载。

  • 管理网络加载过程和完成时候的回调工作。通过addProgressCallback实现。

2.1 SDWebImageDownloaderOptions枚举类型

可以通过这个枚举类型来控制网络加载、请求头、缓存策略等。

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    SDWebImageDownloaderLowPriority = 1 << 0,
    SDWebImageDownloaderProgressiveDownload = 1 << 1,
    /*
     *默认情况下,http请求阻止使用NSURLCache对象。如果设置了这个标记,则NSURLCache会被http请求使用。
     */
    SDWebImageDownloaderUseNSURLCache = 1 << 2,
    /*
     *如果image/imageData是从NSURLCache返回的。则completion这个回调会返回nil。
     */
    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
    /*
     *如果app进入后台模式,是否继续下载。这个是通过在后台申请时间来完成这个操作。如果指定的时间范围内没有完成,则直接取消下载。
     */
    SDWebImageDownloaderContinueInBackground = 1 << 4,
    /*
     处理缓存在`NSHTTPCookieStore`对象里面的cookie。通过设置`NSMutableURLRequest.HTTPShouldHandleCookies = YES`来实现的。
     */
    SDWebImageDownloaderHandleCookies = 1 << 5,
    /*
     *允许非信任的SSL证书请求。
     *在测试的时候很有用。但是正式环境要小心使用。
     */
    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
    /*
     * 默认情况下,图片加载的顺序是根据加入队列的顺序加载的。但是这个标记会把任务加入队列的最前面。
     */
    SDWebImageDownloaderHighPriority = 1 << 7,
    /*
     *默认情况下,图片会按照他的原始大小来解码显示。这个属性会调整图片的尺寸到合适的大小根据设备的内存限制。
     *如果`SDWebImageProgressiveDownload`标记被设置了,则这个flag不起作用。
     */
    SDWebImageDownloaderScaleDownLargeImages = 1 << 8,
};

2.2 SDWebImageDownloader的属性和初始化

可以通过它的属性对最大并行下载数量、超时时间、operation之间的下载顺序、做处理。

/**
 * 当图片下载完成以后,加压缩图片以后再换成。这样可以提升性能但是会占用更多的存储空间。
 * 模式YES,如果你因为过多的内存消耗导致一个奔溃,可以把这个属性设置为NO。
 */
@property (assign, nonatomic) BOOL shouldDecompressImages;

/**
 最大并行下载的数量
 */
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;

/**
 当前并行下载数量
 */
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
/**
 下载超时时间设置
 */
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
/**
 改变下载operation的执行顺序。默认是FIFO。
 */
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;

/**
 单列方法。返回一个单列对象
 @return 返回一个单列的SDWebImageDownloader对象
 */
+ (nonnull instancetype)sharedDownloader;
/**
 为图片加载request设置一个SSL证书对象。
 */
@property (strong, nonatomic, nullable) NSURLCredential *urlCredential;

/**
 Basic认证请求设置用户名和密码
 */
@property (strong, nonatomic, nullable) NSString *username;
@property (strong, nonatomic, nullable) NSString *password;

/**
 * 为http请求设置header。
 * 每一request执行的时候,这个Block都会被执行。用于向http请求添加请求域。
 */
@property (nonatomic, copy, nullable) SDWebImageDownloaderHeadersFilterBlock headersFilter;
/**
 初始化一个请求对象

 @param sessionConfiguration NSURLSessionTask初始化配置
 @return 返回一个SDWebImageDownloader对象
 */
- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration NS_DESIGNATED_INITIALIZER;
/**
 设置请求头域

 @param value 请求头域值
 @param field 请求头域名
 */
- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field;
/*
*获取请求头域的值
*/
- (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field;
/**
 设置一个`SDWebImageDownloaderOperation`的子类作为`NSOperation`来构建request来下载一张图片。

 @param operationClass 指定的子类
 */
- (void)setOperationClass:(nullable Class)operationClass;
/**
 所有的下载图片的Operation都加入NSoperationQueue中
 */
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;

/**
 最后一个添加的Operation
 */
@property (weak, nonatomic, nullable) NSOperation *lastAddedOperation;

/**
 自定义的NSOperation子类
 */
@property (assign, nonatomic, nullable) Class operationClass;

/**
 用于记录url和他对应的SDWebImageDownloaderOperation对象。
 */
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;

/**
 请求头域字典
 */
@property (strong, nonatomic, nullable) SDHTTPHeadersMutableDictionary *HTTPHeaders;
/**
 通过这个`NSURLSession`创建请求
 */
@property (strong, nonatomic) NSURLSession *session;

2.2 downloadImageWithURL方法

这个方法是SDWebImageDownloader的核心方法。SDWebImageManager通过这个方法来实现图片从网络加载。

/**
 新建一个SDWebImageDownloadOperation对象来来做具体的下载操作。同时指定缓存策略、cookie策略、自定义请求头域等。

 @param url url
 @param options 加载选项
 @param progressBlock 进度progress
 @param completedBlock 完成回调
 @return 返回一个SDWebImageDownloadToken,用于关联一个请求
 */
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;

    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;
        }
        /*
         *为了避免可能存在的NSURLCache和SDImageCache同时缓存。我们默认不允许image对象的NSURLCache对象。
         具体缓存策略参考http://www.jianshu.com/p/855c2c6e761f
         */
        NSURLRequestCachePolicy cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
        if (options & SDWebImageDownloaderUseNSURLCache) {
            if (options & SDWebImageDownloaderIgnoreCachedResponse) {
                cachePolicy = NSURLRequestReturnCacheDataDontLoad;
            } else {
                cachePolicy = NSURLRequestUseProtocolCachePolicy;
            }
        }
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
        //使用cookies
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        //使用管道
        request.HTTPShouldUsePipelining = YES;
        //添加自定义请求头
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        //初始化一个自定义NSOperation对象
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        //是否解压缩返回的图片
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        //指定验证信息
        if (sself.urlCredential) {
            //SSL验证
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            //Basic验证
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        //指定优先级
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        //把operatin添加进入NSOperationQueue中
        [sself.downloadQueue addOperation:operation];
        /*
         如果是LIFO这种模式,则需要手动指定operation之间的依赖关系
         */
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            //如果是LIFO,则让前面的operation依赖于最新添加的operation
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }
        return operation;
    }];
}
/**
 给下载过程添加进度

 @param progressBlock 进度Block
 @param completedBlock 完成Block
 @param url url地址
 @param createCallback nil
 @return 返回SDWebImageDownloadToken。方便后面取消
 */
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }

    __block SDWebImageDownloadToken *token = nil;

    dispatch_barrier_sync(self.barrierQueue, ^{
        //看是否当前url是否有对应的Operation图片加载对象
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];
        //如果没有,则直接创建一个。
        if (!operation) {
            //创建一个operation。并且添加到URLOperation中。
            operation = createCallback();
            self.URLOperations[url] = operation;

            __weak SDWebImageDownloaderOperation *woperation = operation;
            //设置operation操作完成以后的回调
            operation.completionBlock = ^{
              SDWebImageDownloaderOperation *soperation = woperation;
              if (!soperation) return;
              if (self.URLOperations[url] == soperation) {
                  [self.URLOperations removeObjectForKey:url];
              };
            };
        }
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    });

    return token;
}

如果要取消一个下载操作,使用cancel方法来处理

/**
 移除一个图片加载操作

 @param token 通过token来确定操作
 */
- (void)cancel:(nullable SDWebImageDownloadToken *)token {
    dispatch_barrier_async(self.barrierQueue, ^{
        SDWebImageDownloaderOperation *operation = self.URLOperations[token.url];
        BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
        if (canceled) {
            [self.URLOperations removeObjectForKey:token.url];
        }
    });
}

另外还有NSURLSession的代理方法,这里就不细讲了。如果兴趣可以参考AFNetWorking源码分析。

3 SDWebImageDownloaderOperation分析

SDWebImageDownloaderOperation是一个自定义、并行的NSOperation子类。这个子类主要实现的功能有:

  • 由于只自定义的并行NSOperation,所以需要管理executing,finished等各种属性的处理,并且手动触发KVO。

  • start(NSOperation规定,没有为什么)方法里面实现主要逻辑。

  • NSURLSessionTaskDelegateNSURLSessionDataDelegate中处理数据的加载,以及进度Block的处理。

  • 如果unownedSession属性因为某种原因是nil,则手动初始化一个做网络请求。

  • 在代理方法中对认证、数据拼装、完成回调Block做处理。

  • 通过发送SDWebImageDownloadStopNotification,SDWebImageDownloadFinishNotification,SDWebImageDownloadReceiveResponseNotification,SDWebImageDownloadStartNotification来通知Operation的状态。

具体完整源码如下:

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

static NSString *const kProgressCallbackKey = @"progress";
static NSString *const kCompletedCallbackKey = @"completed";

typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;

@interface SDWebImageDownloaderOperation ()

/**
 回调Block列表
 */
@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;

/**
 自定义并行Operation需要管理的两个属性。默认是readonly的,我们这里通过声明改为可修改的。方便我们在后面操作。
 */
@property (assign, nonatomic, getter = isExecuting) BOOL executing;
@property (assign, nonatomic, getter = isFinished) BOOL finished;

/**
 存储图片数据
 */
@property (strong, nonatomic, nullable) NSMutableData *imageData;
/**
 通过SDWebImageDownloader传过来。所以这里是weak。因为他是通过SDWebImageDownloader管理的。
 */
@property (weak, nonatomic, nullable) NSURLSession *unownedSession;
/**
 如果unownedSession是nil,我们需要手动创建一个并且管理他的生命周期和代理方法
 */
@property (strong, nonatomic, nullable) NSURLSession *ownedSession;

/**
 dataTask对象
 */
@property (strong, nonatomic, readwrite, nullable) NSURLSessionTask *dataTask;

/**
 一个并行queue。用于控制数据的处理
 */
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t barrierQueue;

#if SD_UIKIT

/**
 如果用户设置了后台继续加载选线。则通过backgroundTask来继续下载图片
 */
@property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
#endif

@end

@implementation SDWebImageDownloaderOperation {
    size_t width, height;
#if SD_UIKIT || SD_WATCH
    UIImageOrientation orientation;
#endif
}

@synthesize executing = _executing;
@synthesize finished = _finished;

- (nonnull instancetype)init {
    return [self initWithRequest:nil inSession:nil options:0];
}

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options {
    if ((self = [super init])) {
        _request = [request copy];
        _shouldDecompressImages = YES;
        _options = options;
        _callbackBlocks = [NSMutableArray new];
        //默认情况下。_executing和finished都是NO
        _executing = NO;
        _finished = NO;
        _expectedSize = 0;
        _unownedSession = session;
        _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
    }
    return self;
}

- (void)dealloc {
    SDDispatchQueueRelease(_barrierQueue);
}

/**
 给Operation添加进度和回调Block

 @param progressBlock 进度Block
 @param completedBlock 回调Block
 @return 回调字典
 */
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    //把Operation对应的回调和进度Block存入一个字典中
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    //把完成和进度Block加入callbackBlocks中
    dispatch_barrier_async(self.barrierQueue, ^{
        [self.callbackBlocks addObject:callbacks];
    });
    return callbacks;
}

- (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
    __block NSMutableArray<id> *callbacks = nil;
    dispatch_sync(self.barrierQueue, ^{
        // We need to remove [NSNull null] because there might not always be a progress block for each callback
        callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
        [callbacks removeObjectIdenticalTo:[NSNull null]];
    });
    return [callbacks copy];    // strip mutability here
}

- (BOOL)cancel:(nullable id)token {
    __block BOOL shouldCancel = NO;
    dispatch_barrier_sync(self.barrierQueue, ^{
        [self.callbackBlocks removeObjectIdenticalTo:token];
        if (self.callbackBlocks.count == 0) {
            shouldCancel = YES;
        }
    });
    if (shouldCancel) {
        [self cancel];
    }
    return shouldCancel;
}

/**
 并行的Operation需要重写这个方法.在这个方法里面做具体的处理
 */
- (void)start {
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

#if SD_UIKIT
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        //如果用户甚至了Background模式,则设置一个backgroundTask
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                //background结束以后。做清理工作
                __strong __typeof (wself) sself = wself;
                if (sself) {
                    [sself cancel];
                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif
        NSURLSession *session = self.unownedSession;
        //如果SDWebImageDownloader传入的session是nil,则自己手动初始化一个。
        if (!self.unownedSession) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            /**
             *  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.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            session = self.ownedSession;
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    //发送请求
    [self.dataTask resume];

    if (self.dataTask) {
        //第一次调用进度BLOCK
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
    } else {
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
    }

#if SD_UIKIT
    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
}

/**
 如果要取消一个Operation,就会调用这个方法。
 */
- (void)cancel {
    @synchronized (self) {
        [self cancelInternal];
    }
}

- (void)cancelInternal {
    if (self.isFinished) return;
    [super cancel];

    if (self.dataTask) {
        [self.dataTask cancel];
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
        });

        // As we cancelled the connection, its callback won't be called and thus won't
        // maintain the isFinished and isExecuting flags.
        //更新状态
        if (self.isExecuting) self.executing = NO;
        if (!self.isFinished) self.finished = YES;
    }

    [self reset];
}

- (void)done {
    self.finished = YES;
    self.executing = NO;
    [self reset];
}

- (void)reset {
    dispatch_barrier_async(self.barrierQueue, ^{
        [self.callbackBlocks removeAllObjects];
    });
    self.dataTask = nil;
    self.imageData = nil;
    if (self.ownedSession) {
        [self.ownedSession invalidateAndCancel];
        self.ownedSession = nil;
    }
}

/**
 需要手动触发_finished的KVO。这个是自定义并发`NSOperation`必须实现的。

 @param finished 改变状态
 */
- (void)setFinished:(BOOL)finished {
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}
/**
 需要手动触发_executing的KVO。这个是自定义并发`NSOperation`必须实现的。
 
 @param executing 改变状态
 */
- (void)setExecuting:(BOOL)executing {
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

/**
 返回YES,表明这个NSOperation对象是并发的

 @return 返回bool值
 */
- (BOOL)isConcurrent {
    return YES;
}

#pragma mark NSURLSessionDataDelegate

/*
 
 */
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    
    //'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;
        //进度回调Block
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, expected, self.request.URL);
        }
        
        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.
        /*
         如果返回304表示图片么有变化。在这种情况下,我们只需要取消operation并且返回缓存的图片就可以了。
         */
        if (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];
    }
    //这个表示允许继续加载
    if (completionHandler) {
        completionHandler(NSURLSessionResponseAllow);
    }
}
/*
 *会被多次调用。获取图片数据
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    
    [self.imageData appendData:data];

    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {
        // 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
        CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
        /*
         *width和height都是0的话表示还么有获取到图片的高度和宽度。我们可以通过数据来获取图片的宽度和高度
         *此时表示第一次收到图片数据
         */
        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 didCompleteWithError.) So save it here and pass it on later.
#if SD_UIKIT || SD_WATCH
                orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
#endif
            }
        }
        /*
         * 这个表示已经收到部分图片数据并且还么有获取到所有的图片数据
         */
        if (width + height > 0 && totalSize < self.expectedSize) {
            // Create the image
            CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);

#if SD_UIKIT || SD_WATCH
            // 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) {
#if SD_UIKIT || SD_WATCH
                UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
#elif SD_MAC
                UIImage *image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
#endif
                //获取图片url对应的key
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                //根据原始图片数据获取对应scale下面的图片
                UIImage *scaledImage = [self scaledImageForKey:key image:image];
                //是否解压缩图片
                if (self.shouldDecompressImages) {
                    /*
                     *解压缩图片
                     */
                    image = [UIImage decodedImageWithImage:scaledImage];
                }
                else {
                    image = scaledImage;
                }
                CGImageRelease(partialImageRef);
                
                [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
            }
        }

        CFRelease(imageSource);
    }

    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
    }
}

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
    //根据request的选项。决定是否缓存NSCachedURLResponse
    NSCachedURLResponse *cachedResponse = proposedResponse;

    if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
        // Prevents caching of responses
        cachedResponse = nil;
    }
    if (completionHandler) {
        completionHandler(cachedResponse);
    }
}

#pragma mark NSURLSessionTaskDelegate
/*
 网络请求加载完成,在这里处理获得的数据
 */
- (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) {
            if (self.imageData) {
                UIImage *image = [UIImage sd_imageWithData:self.imageData];
                //获取url对应的缓存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) {
                        //如果设置了SDWebImageDownloaderScaleDownLargeImages。则返回处理过的图片
                        if (self.options & SDWebImageDownloaderScaleDownLargeImages) {
#if SD_UIKIT || SD_WATCH
                            image = [UIImage decodedAndScaledDownImageWithImage:image];
                            [self.imageData setData:UIImagePNGRepresentation(image)];
#endif
                        } else {
                            image = [UIImage decodedImageWithImage:image];
                        }
                    }
                }
                //构建回调Block
                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];
}

/*
 验证HTTPS的证书
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
    
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;
    //使用可信任证书机构的证书
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        //如果SDWebImageDownloaderAllowInvalidSSLCertificates属性设置了,则不验证SSL证书。直接信任
        if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        } else {
            credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            disposition = NSURLSessionAuthChallengeUseCredential;
        }
    } else {
        //使用自己生成的证书
        if (challenge.previousFailureCount == 0) {
            if (self.credential) {
                credential = self.credential;
                disposition = NSURLSessionAuthChallengeUseCredential;
            } else {
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
        }
    }
    //验证证书
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}
#pragma mark Helper methods

#if SD_UIKIT || SD_WATCH

/**
 把整数转换为对应的枚举值

 @param value 整数值
 @return 枚举值
 */
+ (UIImageOrientation)orientationFromPropertyValue:(NSInteger)value {
    switch (value) {
        case 1:
            return UIImageOrientationUp;
        case 3:
            return UIImageOrientationDown;
        case 8:
            return UIImageOrientationLeft;
        case 6:
            return UIImageOrientationRight;
        case 2:
            return UIImageOrientationUpMirrored;
        case 4:
            return UIImageOrientationDownMirrored;
        case 5:
            return UIImageOrientationLeftMirrored;
        case 7:
            return UIImageOrientationRightMirrored;
        default:
            return UIImageOrientationUp;
    }
}
#endif

/**
* 通过image对象获取对应scale模式下的图像
 */
- (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image {
    return SDScaledImageForKey(key, image);
}

- (BOOL)shouldContinueWhenAppEntersBackground {
    return self.options & SDWebImageDownloaderContinueInBackground;
}

- (void)callCompletionBlocksWithError:(nullable NSError *)error {
    [self callCompletionBlocksWithImage:nil imageData:nil error:error finished:YES];
}
/**
 处理回调

 @param image UIImage数据
 @param imageData Image的data数据
 @param error 错误
 @param finished 是否完成的标记位
 */
- (void)callCompletionBlocksWithImage:(nullable UIImage *)image
                            imageData:(nullable NSData *)imageData
                                error:(nullable NSError *)error
                             finished:(BOOL)finished {
    //获取key对应的回调Block数组
    NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
    dispatch_main_async_safe(^{
        //调用回调
        for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
            completedBlock(image, imageData, error, finished);
        }
    });
}
@end

最后原文地址.html),demo地址

查看原文

赞 0 收藏 1 评论 0

huang303513 发布了文章 · 2017-04-26

AFNetWorking源码之AFSecurityPolicy

1 HTTPS以及SSL/TSL

SSL(Secure Sockets Layer, 安全套接字层),因为原先互联网上使用的HTTP协议是明文的,存在很多缺点,比如传输内容会被偷窥和篡改。SSL协议的作用就是在传输层对网络连接进行加密。

到了1999年,SSL 因为应用广泛,已经成为互联网上的事实标准。IETF就在那年把SSL标准化。标准化之后的名称改为 TLS(Transport Layer Security,传输层安全协议)。SSL与TLS可以视作同一个东西的不同阶段。

简单来说,HTTPS = HTTP + SSL/TLS, 也就是HTTP over SSLHTTP over TLS,这是后面加S的由来。

HTTPS和HTTP异同:HTTP和HTTPS使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。HTTP的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP协议安全。

2 HTTPS的握手

img

1客户端发出请求(ClientHello)

首先,客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做ClientHello请求。在这一步,客户端主要向服务器提供以下信息。

  • (1)支持的协议版本,比如TLS1.0版。

  • (2)一个客户端生成的随机数,稍后用于生成"对话密钥"。

  • (3)支持的加密方法,比如RSA公钥加密。

  • (4)支持的压缩方法。

2服务器回应(SeverHello)

服务器收到客户端请求后,向客户端发出回应,这叫做SeverHello。服务器的回应包含以下内容。

  • (1)确认使用的加密通信协议版本,比如TLS1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。

  • (2)一个服务器生成的随机数,稍后用于生成"对话密钥"。

  • (3)确认使用的加密方法,比如RSA公钥加密。

  • (4)服务器证书。

3客户端回应

客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项信息。

  • (1)一个随机数。该随机数用服务器公钥加密,防止被窃听。

  • (2)编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。

  • (3)客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供服务器校验。

上面第一项的随机数,是整个握手阶段出现的第三个随机数,又称"pre-master key"。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把"会话密钥"。

4服务器的最后回应

服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的"会话密钥"。然后,向客户端最后发送下面信息。

  • (1)编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。

  • (2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。

至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用"会话密钥"加密内容。

3 数字证书

上面握手阶段的第二步服务器给客户端的证书就是数字证书,该证书包含了公钥等信息,一般是由服务器发给客户端,接收方通过验证这个证书是不是由信赖的CA签发,或者与本地的证书相对比,来判断证书是否可信;假如需要双向验证,则服务器和客户端都需要发送数字证书给对方验证。

数字证书是一个电子文档,其中包含了持有者的信息、公钥以及证明该证书有效的数字签名。而数字证书以及相关的公钥管理和验证等技术组成了PKI(公钥基础设施)规范体系。一般来说,数字证书是由数字证书认证机构(Certificate authority,即CA)来负责签发和管理,并承担PKI体系中公钥合法性的检验责任;数字证书的类型有很多,而HTTPS使用的是SSL证书。

怎么来验证数字证书是由CA签发的,而不是第三方伪造的呢? 在回答这个问题前,我们需要先了解CA的组织结构。首先,CA组织结构中,最顶层的就是根CA,根CA下可以授权给多个二级CA,而二级CA又可以授权多个三级CA,所以CA的组织结构是一个树结构。对于SSL证书市场来说,主要被Symantec(旗下有VeriSign和GeoTrust)、Comodo SSL、Go Daddy 和 GlobalSign 瓜分。 了解了CA的组织结构后,来看看数字证书的签发流程:

img

数字证书的签发机构CA,在接收到申请者的资料后进行核对并确定信息的真实有效,然后就会制作一份符合X.509标准的文件。证书中的证书内容包括了持有者信息和公钥等都是由申请者提供的,而数字签名则是CA机构对证书内容进行hash加密后等到的,而这个数字签名就是我们验证证书是否是有可信CA签发的数据。

img

接收端接到一份数字证书Cer1后,对证书的内容做Hash等到H1;然后在签发该证书的机构CA1的数字证书中找到公钥,对证书上数字签名进行解密,得到证书Cer1签名的Hash摘要H2;对比H1和H2,假如相等,则表示证书没有被篡改。但这个时候还是不知道CA是否是合法的,我们看到上图中有CA机构的数字证书,这个证书是公开的,所有人都可以获取到。而这个证书中的数字签名是上一级生成的,所以可以这样一直递归验证下去,直到根CA。根CA是自验证的,即他的数字签名是由自己的私钥来生成的。合法的根CA会被浏览器和操作系统加入到权威信任CA列表中,这样就完成了最终的验证。所以,一定要保护好自己环境(浏览器/操作系统)中根CA信任列表,信任了根CA就表示信任所有根CA下所有子级CA所签发的证书,不要随便添加根CA证书。

4 SSL Pinning

可以理解为证书绑定,是指客户端直接保存服务端的证书,建立https连接时直接对比服务端返回的和客户端保存的两个证书是否一样,一样就表明证书是真的,不再去系统的信任证书机构里寻找验证。这适用于非浏览器应用,因为浏览器跟很多未知服务端打交道,无法把每个服务端的证书都保存到本地,但CS架构的像手机APP事先已经知道要进行通信的服务端,可以直接在客户端保存这个服务端的证书用于校验。

为什么直接对比就能保证证书没问题?如果中间人从客户端取出证书,再伪装成服务端跟其他客户端通信,它发送给客户端的这个证书不就能通过验证吗?确实可以通过验证,但后续的流程走不下去,因为下一步客户端会用证书里的公钥加密,中间人没有这个证书的私钥就解不出内容,也就截获不到数据,这个证书的私钥只有真正的服务端有,中间人伪造证书主要伪造的是公钥。

为什么要用SSL Pinning?正常的验证方式不够吗?如果服务端的证书是从受信任的的CA机构颁发的,验证是没问题的,但CA机构颁发证书比较昂贵,小企业或个人用户可能会选择自己颁发证书,这样就无法通过系统受信任的CA机构列表验证这个证书的真伪了,所以需要SSL Pinning这样的方式去验证。

5 iOS的HTTPS请求

下面我会实现自签名证书(12306)、SSL信任证书(baidu)、系统证书(苹果)三种情况的实现来看他们的区别,百度和12306的证书已经被我下载到我的项目里面了,具体可以去Demo里面看实现过程。

1 自签名证书

我们手动指定securityPolicy认证属性。通过12306证书来实现。

//自建证书认证
- (IBAction)buttion1:(id)sender {
    NSURL *url = [NSURL URLWithString:@"https://kyfw.12306.cn/otn/leftTicket/init"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
   // [request setValue:@"text/html" forHTTPHeaderField:@"Accept"];
    AFURLSessionManager *manager = [[AFURLSessionManager alloc]initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    //指定安全策略
    manager.securityPolicy = [self ticketSecurityPolicy];
    //指定返回数据类型,默认是AFJSONResponseSerializer类型,犹豫这里不是JSON类型的返回数据,所以需要手动指定返回类型
    AFHTTPResponseSerializer *responseSerializer = [AFHTTPResponseSerializer serializer];
    responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
    manager.responseSerializer = responseSerializer;
    NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
        NSLog(@"%@-----%@",[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding],error);
    }];
    [dataTask resume];

}
/**
 12306的认证证书,他的认证证书是自签名的

 @return 返回指定的认证策略
 */
-(AFSecurityPolicy*)ticketSecurityPolicy {
    // /先导入证书
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"12306" ofType:@"cer"];//证书的路径
    NSData *certData = [NSData dataWithContentsOfFile:cerPath];
    NSSet *set = [NSSet setWithObject:certData];
    
    AFSecurityPolicy *securityPolicy;
    if (true) {
        securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:set];
    }else{
        // AFSSLPinningModeCertificate 使用证书验证模式。下面这个方法会默认使用项目里面的所有证书
        securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    }
    // allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO
    // 如果是需要验证自建证书,需要设置为YES
    securityPolicy.allowInvalidCertificates = YES;
    
    //validatesDomainName 是否需要验证域名,默认为YES;
    //假如证书的域名与你请求的域名不一致,需把该项设置为NO;如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。
    //置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
    //如置为NO,建议自己添加对应域名的校验逻辑。
    securityPolicy.validatesDomainName = NO;
    
    return securityPolicy;
}

2 SSL信任证书

我们手动指定securityPolicy认证属性。通过百度证书来实现。

//认证证书认证
- (IBAction)button2:(id)sender {
    NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    //[request setValue:@"text/html" forHTTPHeaderField:@"Accept"];
    AFURLSessionManager *manager = [[AFURLSessionManager alloc]initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    //指定安全策略
    manager.securityPolicy = [self baiduSecurityPolicy];
    //指定返回数据类型,默认是AFJSONResponseSerializer类型,犹豫这里不是JSON类型的返回数据,所以需要手动指定返回类型
    AFHTTPResponseSerializer *responseSerializer = [AFHTTPResponseSerializer serializer];
    responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
    manager.responseSerializer = responseSerializer;
    NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
        NSLog(@"%@-----%@",[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding],error);
    }];
    [dataTask resume];
}

/**
百度的的认证证书,他的认证证书是花钱买的,也就是不是自签名的证书。这种证书,如果我们要手动指定,pinmode只能是`AFSSLPinningModeNone`
 
 @return 返回指定的认证策略
 */
-(AFSecurityPolicy*)baiduSecurityPolicy {
    // /先导入证书
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"baidu" ofType:@"cer"];//证书的路径
    NSData *certData = [NSData dataWithContentsOfFile:cerPath];
    NSSet *set = [NSSet setWithObject:certData];
    
    AFSecurityPolicy *securityPolicy;
    if (true) {
        //这里只能用AFSSLPinningModeNone才能成功,而且我系统的证书列表里面已经有百度的证书了
        securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone withPinnedCertificates:set];
    }else{
        // AFSSLPinningModeCertificate 使用证书验证模式。下面这个方法会默认使用项目里面的所有证书
        securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
    }
    // allowInvalidCertificates 是否允许无效证书(也就是自建的证书),默认为NO
    // 如果是需要验证自建证书,需要设置为YES
    securityPolicy.allowInvalidCertificates = NO;
    
    //validatesDomainName 是否需要验证域名,默认为YES;
    //假如证书的域名与你请求的域名不一致,需把该项设置为NO;如设成NO的话,即服务器使用其他可信任机构颁发的证书,也可以建立连接,这个非常危险,建议打开。
    //置为NO,主要用于这种情况:客户端请求的是子域名,而证书上的是另外一个域名。因为SSL证书上的域名是独立的,假如证书上注册的域名是www.google.com,那么mail.google.com是无法验证通过的;当然,有钱可以注册通配符的域名*.google.com,但这个还是比较贵的。
    //如置为NO,建议自己添加对应域名的校验逻辑。
    securityPolicy.validatesDomainName = YES;
    
    return securityPolicy;
}

3 SSL证书AFN默认处理

这里我们不做任何额外的处理,直接使用AFN的默认证书处理机制。通过AFURLSessionManagersecurityPolicy默认实现。它会和存在系统中的做对比来验证证书。

//系统证书认证
- (IBAction)button3:(id)sender {
    NSURL *url = [NSURL URLWithString:@"https://www.apple.com/"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    AFURLSessionManager *manager = [[AFURLSessionManager alloc]initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    //指定返回数据类型,默认是AFJSONResponseSerializer类型,犹豫这里不是JSON类型的返回数据,所以需要手动指定返回类型
    AFHTTPResponseSerializer *responseSerializer = [AFHTTPResponseSerializer serializer];
    responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"text/html"];
    manager.responseSerializer = responseSerializer;
    NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id  _Nullable responseObject, NSError * _Nullable error) {
        NSLog(@"%@-----%@",[[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding],error);
    }];
    [dataTask resume];
}

6 AFSecurityPolicy源码解析

AFSecurityPolicy分三种验证模式

  • AFSSLPinningModeNone:

    这个模式表示不做SSL pinning,只跟浏览器一样在系统的信任机构列表里验证服务端返回的证书。若证书是信任机构签发的就会通过,若是自己服务器生成的证书,这里是不会通过的。
  • AFSSLPinningModeCertificate:

    这个模式表示用证书绑定方式验证证书,需要客户端保存有服务端的证书拷贝,这里验证分两步,第一步验证证书的域名/有效期等信息,第二步是对比服务端返回的证书跟客户端返回的是否一致。这里还没弄明白第一步的验证是怎么进行的,代码上跟去系统信任机构列表里验证一样调用了SecTrustEvaluate,只是这里的列表换成了客户端保存的那些证书列表。若要验证这个,是否应该把服务端证书的颁发机构根证书也放到客户端里?
  • AFSSLPinningModePublicKey:

    这个模式同样是用证书绑定方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据。
    

SecTrustRef

这是一个需要验证的信任对象,包含待验证的证书和支持的验证方法等。

SecTrustResultType

表示验证结果。其中 kSecTrustResultProceed表示serverTrust验证成功,且该验证得到了用户认可(例如在弹出的是否信任的alert框中选择always trust)。 kSecTrustResultUnspecified表示 serverTrust验证成功,此证书也被暗中信任了,但是用户并没有显示地决定信任该证书。 两者取其一就可以认为对serverTrust验证成功。

SecTrustEvaluate

证书校验函数,在函数的内部递归地从叶节点证书到根证书验证。需要验证证书本身的合法性(验证签名完整性,验证证书有效期等);验证证书颁发者的合法性(查找颁发者的证书并检查其合法性,这个过程是递归的).而递归的终止条件是证书验证过程中遇到了锚点证书(锚点证书:嵌入到操作系统中的根证书,这个根证书是权威证书颁发机构颁发的自签名证书)。

AFSecurityPolicy的源码细节如下:

/**
 证书的验证类型

 - AFSSLPinningModeNone: 不使用`pinned certificates`来验证证书
 - AFSSLPinningModePublicKey: 使用`pinned certificates`来验证证书的公钥
 - AFSSLPinningModeCertificate: 使用`pinned certificates`来验证整个证书
 */
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
    AFSSLPinningModeNone,
    AFSSLPinningModePublicKey,
    AFSSLPinningModeCertificate,
};

/**
 获取指定证书的公钥

 @param certificate 证书数据
 @return 公钥
 */
static id AFPublicKeyForCertificate(NSData *certificate) {
    id allowedPublicKey = nil;
    SecCertificateRef allowedCertificate;
    SecPolicyRef policy = nil;
    SecTrustRef allowedTrust = nil;
    SecTrustResultType result;
    //获取证书对象
    allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);
    __Require_Quiet(allowedCertificate != NULL, _out);
    //获取X.509的认证策略
    policy = SecPolicyCreateBasicX509();
    //获取allowedTrust对象的值
    __Require_noErr_Quiet(SecTrustCreateWithCertificates(allowedCertificate, policy, &allowedTrust), _out);
    __Require_noErr_Quiet(SecTrustEvaluate(allowedTrust, &result), _out);
    //根据allowedTrust获取对应的公钥
    allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);
//C++的gumpto跳转,当前面的操作出错以后,直接跳入_out执行
_out:
    if (allowedTrust) {
        CFRelease(allowedTrust);
    }
    if (policy) {
        CFRelease(policy);
    }
    if (allowedCertificate) {
        CFRelease(allowedCertificate);
    }
    //返回公钥
    return allowedPublicKey;
}

/**
 在指定的证书和认证策略下,验证SecTrustRef对象是否是受信任的、合法的。

 @param serverTrust SecTrustRef对象
 @return 结果
 */
static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
    BOOL isValid = NO;
    SecTrustResultType result;
    //获取serverTrust的认证结果,调用`SecTrustEvaluate`表示通过系统的证书来比较认证
    __Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);
    isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);

_out:
    return isValid;
}

/**
 根据`serverTrust`获取认证的证书链

 @param serverTrust serverTrust对象
 @return 认证证书链
 */
static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {
    //获取认证链的总层次
    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
    //获取每一级认证链,把获取的证书数据存入数组中
    for (CFIndex i = 0; i < certificateCount; i++) {
        SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
        [trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)];
    }
    //返回证书链数组
    return [NSArray arrayWithArray:trustChain];
}

/**
 获取serverTrust对象的认证链的公钥数组

 @param serverTrust serverTrust对象
 @return 公钥数组
 */
static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) {
    //X.509标准的安全策略
    SecPolicyRef policy = SecPolicyCreateBasicX509();
    //获取证书链的证书数量
    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
    for (CFIndex i = 0; i < certificateCount; i++) {
        SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);

        SecCertificateRef someCertificates[] = {certificate};
        CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL);

        SecTrustRef trust;
        //通过一个证书、认证策略新建一个SecTrustRef对象
        __Require_noErr_Quiet(SecTrustCreateWithCertificates(certificates, policy, &trust), _out);
        
        SecTrustResultType result;
        //验证SecTrustRef对象是否成功
        __Require_noErr_Quiet(SecTrustEvaluate(trust, &result), _out);
        //把SecTrustRef对应的公钥加入数组中
        [trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];

    _out:
        if (trust) {
            CFRelease(trust);
        }

        if (certificates) {
            CFRelease(certificates);
        }

        continue;
    }
    CFRelease(policy);

    return [NSArray arrayWithArray:trustChain];
}

#pragma mark -

@interface AFSecurityPolicy()
//认证策略
@property (readwrite, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
//公钥集合
@property (readwrite, nonatomic, strong) NSSet *pinnedPublicKeys;
@end

@implementation AFSecurityPolicy
/**
 从MainBundle中获取所有证书
 
 @param bundle 返回包含在bundle中的证书集合。如果AFNetworking使用的是静态库,我们必须通过这个方法来加载证书。并且通过`policyWithPinningMode:withPinnedCertificates`方法来指定认证类型。
 @return 返回bundle里面的证书
 */
+ (NSSet *)certificatesInBundle:(NSBundle *)bundle {
    //获取项目里的所有.cer证书
    NSArray *paths = [bundle pathsForResourcesOfType:@"cer" inDirectory:@"."];
    NSMutableSet *certificates = [NSMutableSet setWithCapacity:[paths count]];
    for (NSString *path in paths) {
        //获取证书对应的NSData,并且加入集合中
        NSData *certificateData = [NSData dataWithContentsOfFile:path];
        [certificates addObject:certificateData];
    }
    //返回证书集合
    return [NSSet setWithSet:certificates];
}
/**
 返回当前类所在bundle所在的证书集合
 
 @return 证书集合
 */
+ (NSSet *)defaultPinnedCertificates {
    static NSSet *_defaultPinnedCertificates = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //获取当前类所在bundle
        NSBundle *bundle = [NSBundle bundleForClass:[self class]];
        _defaultPinnedCertificates = [self certificatesInBundle:bundle];
    });

    return _defaultPinnedCertificates;
}
/**
 返回默认的安全认证策略,在这里是验证系统的证书。这个策略不允许非法证书、验证主机名、不验证证书内容和公钥
 
 @return 返回认证策略
 */
+ (instancetype)defaultPolicy {
    AFSecurityPolicy *securityPolicy = [[self alloc] init];
    securityPolicy.SSLPinningMode = AFSSLPinningModeNone;

    return securityPolicy;
}

/**
 根据指定的认证策略和默认的证书列表初始化一个`AFSecurityPolicy`对象

 @param pinningMode 认证策略
 @return `AFSecurityPolicy`对象
 */
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode {
    return [self policyWithPinningMode:pinningMode withPinnedCertificates:[self defaultPinnedCertificates]];
}

/**
 通过制定的认证策略`pinningMode`和证书集合`pinnedCertificates`来初始化一个`AFSecurityPolicy`对象

 @param pinningMode 认证模型
 @param pinnedCertificates 证书集合
 @return AFSecurityPolicy对象
 */
+ (instancetype)policyWithPinningMode:(AFSSLPinningMode)pinningMode withPinnedCertificates:(NSSet *)pinnedCertificates {
    AFSecurityPolicy *securityPolicy = [[self alloc] init];
    securityPolicy.SSLPinningMode = pinningMode;
    //设置`_pinnedCertificates`和`pinnedPublicKeys`属性,分别对应证书集合和公钥集合
    [securityPolicy setPinnedCertificates:pinnedCertificates];
    //返回初始化成功的`AFSecurityPolicy`
    return securityPolicy;
}

- (instancetype)init {
    self = [super init];
    if (!self) {
        return nil;
    }
    //默认是要认证主机名称
    self.validatesDomainName = YES;
    
    return self;
}

/**
通过指定的证书结合获取到对应的公钥集合。然后赋值给`pinnedPublicKeys`属性
 @param pinnedCertificates 证书集合
 */
- (void)setPinnedCertificates:(NSSet *)pinnedCertificates {
    _pinnedCertificates = pinnedCertificates;

    if (self.pinnedCertificates) {
        NSMutableSet *mutablePinnedPublicKeys = [NSMutableSet setWithCapacity:[self.pinnedCertificates count]];
        //迭代每一个证书
        for (NSData *certificate in self.pinnedCertificates) {
            //获取证书对应的公钥
            id publicKey = AFPublicKeyForCertificate(certificate);
            if (!publicKey) {
                continue;
            }
            [mutablePinnedPublicKeys addObject:publicKey];
        }
        //赋值给对应的属性
        self.pinnedPublicKeys = [NSSet setWithSet:mutablePinnedPublicKeys];
    } else {
        self.pinnedPublicKeys = nil;
    }
}

#pragma mark -
/**
 为serverTrust对象指定认证策略,如果domain不为nil,则包括对主机名的认证。这个方法必须在接受到`authentication challenge`返回的时候调用。
 SecTrustRef可以理解为桥接证书与认证策略的对象,他关联指定的证书与认证策略
 
 @param serverTrust 服务器的X.509标准的证书数据
 @param domain 认证服务器的主机名。如果是nil,则不会对主机名进行认证。
 @return serverTrust是否通过认证
 */
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
        // https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
        //  According to the docs, you should only trust your provided certs for evaluation.
        //  Pinned certificates are added to the trust. Without pinned certificates,
        //  there is nothing to evaluate against.
        //
        //  From Apple Docs:
        //          "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
        //           Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
        NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
        return NO;
    }

    NSMutableArray *policies = [NSMutableArray array];
    if (self.validatesDomainName) {
        //使用需要认证主机名的认证策略
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        //使用默认的认证策略
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }
    //给serverTrust对象指定认证策略
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
    }
    //根据证书验证策略、数字签名认证策略、其他认证策略来处理不同情况
    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone://不验证公钥和证书
        default:
            return NO;
        case AFSSLPinningModeCertificate: {//验证整个证书
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            //根据指定证书获取,获取对应的证书对象
            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            //把证书与serverTrust关联起来
            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);

            if (!AFServerTrustIsValid(serverTrust)) {
                return NO;
            }

            // obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
            //获取serverTrust证书链。直到根证书。
            NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
            //如果`pinnedCertificates`包含`serverTrust`对象对应的证书链的根证书。则返回true
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }
            
            return NO;
        }
        case AFSSLPinningModePublicKey: {//只验证证书里面的数字签名
            NSUInteger trustedPublicKeyCount = 0;
            //根据serverTrust对象和SecPolicyCreateBasicX509认证策略,获取对应的公钥集合
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
            
            for (id trustChainPublicKey in publicKeys) {
                //把获取的公钥和系统获取的默认公钥比较,如果相等,则通过认证
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }
            return trustedPublicKeyCount > 0;
        }
    }
    
    return NO;
}

#pragma mark - NSKeyValueObserving

+ (NSSet *)keyPathsForValuesAffectingPinnedPublicKeys {
    return [NSSet setWithObject:@"pinnedCertificates"];
}

#pragma mark - NSSecureCoding

+ (BOOL)supportsSecureCoding {
    return YES;
}

- (instancetype)initWithCoder:(NSCoder *)decoder {

    self = [self init];
    if (!self) {
        return nil;
    }

    self.SSLPinningMode = [[decoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(SSLPinningMode))] unsignedIntegerValue];
    self.allowInvalidCertificates = [decoder decodeBoolForKey:NSStringFromSelector(@selector(allowInvalidCertificates))];
    self.validatesDomainName = [decoder decodeBoolForKey:NSStringFromSelector(@selector(validatesDomainName))];
    self.pinnedCertificates = [decoder decodeObjectOfClass:[NSArray class] forKey:NSStringFromSelector(@selector(pinnedCertificates))];

    return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
    [coder encodeObject:[NSNumber numberWithUnsignedInteger:self.SSLPinningMode] forKey:NSStringFromSelector(@selector(SSLPinningMode))];
    [coder encodeBool:self.allowInvalidCertificates forKey:NSStringFromSelector(@selector(allowInvalidCertificates))];
    [coder encodeBool:self.validatesDomainName forKey:NSStringFromSelector(@selector(validatesDomainName))];
    [coder encodeObject:self.pinnedCertificates forKey:NSStringFromSelector(@selector(pinnedCertificates))];
}
#pragma mark - NSCopying
- (instancetype)copyWithZone:(NSZone *)zone {
    AFSecurityPolicy *securityPolicy = [[[self class] allocWithZone:zone] init];
    securityPolicy.SSLPinningMode = self.SSLPinningMode;
    securityPolicy.allowInvalidCertificates = self.allowInvalidCertificates;
    securityPolicy.validatesDomainName = self.validatesDomainName;
    securityPolicy.pinnedCertificates = [self.pinnedCertificates copyWithZone:zone];

    return securityPolicy;
}
@end

最后原文地址,demo地址

查看原文

赞 1 收藏 2 评论 0

huang303513 发布了文章 · 2017-04-25

AFNetWorking源码之AFURLRequestSerialization

1 概述

AFURLRequestSerialization主要实现了根据不同情况和参数初始化NSURLRequest对象的功能。只有AFHTTPSessionManager有requestSerialization,默认是AFHTTPRequestSerializer对象。尤其是我们使用MultipartForm请求的时候,可以使用它帮我们完成繁杂的请求头拼接过程,这个是最值得推荐的。

在阅读源码之前,一定要对multipart/form-data非常熟悉,不然会有很多地方看不懂。具体可以看AFNetWorking源码之AFHTTPSessionManager关于它的那部分。

2 AFURLRequestSerialization的api分析

AFURLRequestSerialization包含了四个部分:

  • 全局方法:AFPercentEscapedStringFromStringAFQueryStringFromParameters

  • 协议AFURLRequestSerialization提供了一个序列化parameters参数的方法。我们可以把参数转换为查询字符串、HTTP请求体、设置恰当的请求头等。

  • AFHTTPRequestSerializer继承自AFURLRequestSerialization协议。提供了查询字符串/URL格式的参数序列化、默认请求头处理。同时以提供HTTP状态码和返回数据的验证等工作。
    _ AFMultipartFormData协议。主要用于添加multipart/form-data请求的Content-Disposition: file; filename=#{generated filename}; name=#{name}"Content-Type: #{generated mimeType}的请求体域。

  • 类型AFJSONRequestSerializerAFPropertyListRequestSerializer。主要针对JSON和Plist类型的序列化优化。

AFPercentEscapedStringFromString返回一个字符串的百分号编码格式的字符串。因为url只有普通英文字符和数字,特殊字符$-_.+!*'()还有保留字符。所以很多字符都需要编码,非ASCII编码的字符串先转换为ASCII编码,然后再转换为百分号编码。

/**
AFPercentEscapedStringFromString方法的作用就是把一个普通字符串转换为百分号编码的字符串
 http://blog.csdn.net/qq_32010299/article/details/51790407
 @param string 一个字符串
 @return 百分号编码的字符串
 */
NSString * AFPercentEscapedStringFromString(NSString *string) {
    //可能需要做百分号编码处理的字符串
    static NSString * const kAFCharactersGeneralDelimitersToEncode = @":#[]@"; 
    static NSString * const kAFCharactersSubDelimitersToEncode = @"!$&'()*+,;=";
    //不需要做百分号编码的字符串集合
    NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
    //获取目前系统中最终需要做百分号编码转换的字符集合
    [allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];

    static NSUInteger const batchSize = 50;
    NSUInteger index = 0;
    NSMutableString *escaped = @"".mutableCopy;
    //迭代字符串做百分号编码
    while (index < string.length) {
        NSUInteger length = MIN(string.length - index, batchSize);
        NSRange range = NSMakeRange(index, length);
        //移除字符串中的一些非法字符。比如????
        range = [string rangeOfComposedCharacterSequencesForRange:range];
        NSString *substring = [string substringWithRange:range];
        //指定范围内的字符做百分号编码
        NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
        [escaped appendString:encoded];
        index += range.length;
    }
    //返回处理以后的字符串
    return escaped;
}

私有类AFQueryStringPair的主要功能就是把一个key和vaue的键值对转换为百分号编码格式的键值对并且用=链接起来

@interface AFQueryStringPair : NSObject
@property (readwrite, nonatomic, strong) id field;
@property (readwrite, nonatomic, strong) id value;
- (instancetype)initWithField:(id)field value:(id)value;
- (NSString *)URLEncodedStringValue;
@end
@implementation AFQueryStringPair
- (instancetype)initWithField:(id)field value:(id)value {
    self = [super init];
    if (!self) {
        return nil;
    }
    self.field = field;
    self.value = value;
    return self;
}
/**
 把key、value键值对转换为百分号编码,并且链接起来
 @return 转换后的字符串
 */
- (NSString *)URLEncodedStringValue {
    if (!self.value || [self.value isEqual:[NSNull null]]) {
        return AFPercentEscapedStringFromString([self.field description]);
    } else {
        //先用百分号编码处理,然后再拼接
        return [NSString stringWithFormat:@"%@=%@", AFPercentEscapedStringFromString([self.field description]), AFPercentEscapedStringFromString([self.value description])];
    }
}
@end

方法AFQueryStringPairsFromDictionaryAFQueryStringPairsFromKeyAndValue分别把一个字典或者key、value键值对转换为url的query参数。

/**
 把一个字典转换为百分号编码的query参数
 @param parameters 要转换的字典
 @return query参数
 */
NSString * AFQueryStringFromParameters(NSDictionary *parameters) {
    NSMutableArray *mutablePairs = [NSMutableArray array];
    for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
        //调用`AFQueryStringPair`序列化
        [mutablePairs addObject:[pair URLEncodedStringValue]];
    }
    return [mutablePairs componentsJoinedByString:@"&"];
}
NSArray * AFQueryStringPairsFromDictionary(NSDictionary *dictionary) {
    return AFQueryStringPairsFromKeyAndValue(nil, dictionary);
}
/**
 分别把一个字典、数组、集合转换为一个AFQueryStringPair对象的的数组。
 @param key key
 @param value value
 @return AFQueryStringPair类型数组
 */
NSArray * AFQueryStringPairsFromKeyAndValue(NSString *key, id value) {
    NSMutableArray *mutableQueryStringComponents = [NSMutableArray array];
    //使用`description`排序
    NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"description" ascending:YES selector:@selector(compare:)];
    if ([value isKindOfClass:[NSDictionary class]]) {
        NSDictionary *dictionary = value;
        for (id nestedKey in [dictionary.allKeys sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
            id nestedValue = dictionary[nestedKey];
            if (nestedValue) {
                //如果是字典,就取出每一对key、value处理
                [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue((key ? [NSString stringWithFormat:@"%@[%@]", key, nestedKey] : nestedKey), nestedValue)];
            }
        }
    } else if ([value isKindOfClass:[NSArray class]]) {
        NSArray *array = value;
        for (id nestedValue in array) {
            //如果是数组,则取出元素,添加一个额外的key处理
            [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue([NSString stringWithFormat:@"%@[]", key], nestedValue)];
        }
    } else if ([value isKindOfClass:[NSSet class]]) {
        NSSet *set = value;
        for (id obj in [set sortedArrayUsingDescriptors:@[ sortDescriptor ]]) {
            //如果是集合,就是用默认key和集合元素处理
            [mutableQueryStringComponents addObjectsFromArray:AFQueryStringPairsFromKeyAndValue(key, obj)];
        }
    } else {
        //添加处理后的key和value
        [mutableQueryStringComponents addObject:[[AFQueryStringPair alloc] initWithField:key value:value]];
    }
    //返回`AFQueryStringPair`对象数组
    return mutableQueryStringComponents;
}

AFHTTPRequestSerializerObservedKeyPaths全局方法指定了request请求序列化要观察的属性列表、是一个数组,里面有对蜂窝数据、缓存策略、cookie、管道、网络状态、超时这几个元素。

static NSArray * AFHTTPRequestSerializerObservedKeyPaths() {
    static NSArray *_AFHTTPRequestSerializerObservedKeyPaths = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _AFHTTPRequestSerializerObservedKeyPaths = @[NSStringFromSelector(@selector(allowsCellularAccess)), NSStringFromSelector(@selector(cachePolicy)), NSStringFromSelector(@selector(HTTPShouldHandleCookies)), NSStringFromSelector(@selector(HTTPShouldUsePipelining)), NSStringFromSelector(@selector(networkServiceType)), NSStringFromSelector(@selector(timeoutInterval))];
    });

    return _AFHTTPRequestSerializerObservedKeyPaths;
}

2.1 AFHTTPRequestSerializer的解析

AFHTTPRequestSerializer主要实现了大部分request拼接转化功能。比如通用请求头的添加如userAgent、request属性的KVO观察、手动指定请求头序列化的Block、负责具体的request对象的初始化等。

1 AFHTTPRequestSerializer的属性和初始化

//属性列表
@interface AFHTTPRequestSerializer ()
//某个request需要观察的属性集合
@property (readwrite, nonatomic, strong) NSMutableSet *mutableObservedChangedKeyPaths;
//存储request的请求头域
@property (readwrite, nonatomic, strong) NSMutableDictionary *mutableHTTPRequestHeaders;
//用于修改或者设置请求体域的dispatch_queue_t。
@property (readwrite, nonatomic, strong) dispatch_queue_t requestHeaderModificationQueue;
@property (readwrite, nonatomic, assign) AFHTTPRequestQueryStringSerializationStyle queryStringSerializationStyle;
//手动指定parameters参数序列化的Block
@property (readwrite, nonatomic, copy) AFQueryStringSerializationBlock queryStringSerialization;
@end
//初始化方法
- (instancetype)init {
    self = [super init];
    if (!self) {
        return nil;
    }
    //指定序列化编码格式
    self.stringEncoding = NSUTF8StringEncoding;
    //请求头保存在一个字典中,方便后面构建request的时候拼装。
    self.mutableHTTPRequestHeaders = [NSMutableDictionary dictionary];
    //初始化一个操作request的header域的dispatch_queue_t
    self.requestHeaderModificationQueue = dispatch_queue_create("requestHeaderModificationQueue", DISPATCH_QUEUE_CONCURRENT);

    NSMutableArray *acceptLanguagesComponents = [NSMutableArray array];
    /*
     *枚举系统的language列表。然后设置`Accept-Language`请求头域。优先级逐级降低,最多五个。
     */
    [[NSLocale preferredLanguages] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        float q = 1.0f - (idx * 0.1f);
        [acceptLanguagesComponents addObject:[NSString stringWithFormat:@"%@;q=%0.1g", obj, q]];
        *stop = q <= 0.5f;
    }];
    //数组元素使用`, `分割
    [self setValue:[acceptLanguagesComponents componentsJoinedByString:@", "] forHTTPHeaderField:@"Accept-Language"];
    /*
     *设置User-Agent请求头域的值。
     */
    NSString *userAgent = nil;
    userAgent = [NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], [[UIScreen mainScreen] scale]];
    if (userAgent) {
        /*
         *如果userAgent里面包含非ASCII码的字符,比如中文,则需要转换。这里是转换为对应的拉丁字母。
         AFNetWorking3.X源码阅读/1.0 (iPhone; iOS 10.2; Scale/2.00)
         AFNetWorking3.X yuan ma yue du/1.0 (iPhone; iOS 10.2; Scale/2.00)
         */
        if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) {
            NSMutableString *mutableUserAgent = [userAgent mutableCopy];
            //转换为拉丁字母
            if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) {
                userAgent = mutableUserAgent;
            }
        }
        [self setValue:userAgent forHTTPHeaderField:@"User-Agent"];
    }

    // HTTP Method Definitions; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html
    //需要把parameters转换为query参数的方法集合。
    self.HTTPMethodsEncodingParametersInURI = [NSSet setWithObjects:@"GET", @"HEAD", @"DELETE", nil];

    self.mutableObservedChangedKeyPaths = [NSMutableSet set];
    /*
     添加对蜂窝数据、缓存策略、cookie、管道、网络状态、超时这几个属性的观察。
     */
    for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
        if ([self respondsToSelector:NSSelectorFromString(keyPath)]) {
            [self addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:AFHTTPRequestSerializerObserverContext];
        }
    }
    return self;
}

2 AFHTTPRequestSerializer的各种setter方法

首先通过automaticallyNotifiesObserversForKey方法来阻止一些属性的KVO机制的触发,然后我们通过重写蜂窝数据、缓存策略、cookie、管道、网络状态、超时的观察。可以用于测试这些属性变化是否崩溃等。

/**
 如果kvo的触发机制是默认出发。则返回true,否则返回false。在这里,只要是`AFHTTPRequestSerializerObservedKeyPaths`里面的属性,我们都取消自动出发kvo机制,使用手动触发。

 @param key kvo的key
 @return bool值
 */
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([AFHTTPRequestSerializerObservedKeyPaths() containsObject:key]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(__unused id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
    //是否是选择要观察的属性
    if (context == AFHTTPRequestSerializerObserverContext) {
        //如果属性值为null,则表示么有这个属性,移除对其的观察
        if ([change[NSKeyValueChangeNewKey] isEqual:[NSNull null]]) {
            [self.mutableObservedChangedKeyPaths removeObject:keyPath];
        } else {
            //添加到要观察的属性的集合
            [self.mutableObservedChangedKeyPaths addObject:keyPath];
        }
    }
}

通过重写属性的setter方法来手动触发kvo

#pragma mark - 手动触发蜂窝数据、缓存策略、cookie、管道、网络状态、超时的观察。可以用于测试这些属性变化是否崩溃等。
- (void)setAllowsCellularAccess:(BOOL)allowsCellularAccess {
    [self willChangeValueForKey:NSStringFromSelector(@selector(allowsCellularAccess))];
    _allowsCellularAccess = allowsCellularAccess;
    [self didChangeValueForKey:NSStringFromSelector(@selector(allowsCellularAccess))];
}

- (void)setCachePolicy:(NSURLRequestCachePolicy)cachePolicy {
    [self willChangeValueForKey:NSStringFromSelector(@selector(cachePolicy))];
    _cachePolicy = cachePolicy;
    [self didChangeValueForKey:NSStringFromSelector(@selector(cachePolicy))];
}

- (void)setHTTPShouldHandleCookies:(BOOL)HTTPShouldHandleCookies {
    [self willChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldHandleCookies))];
    _HTTPShouldHandleCookies = HTTPShouldHandleCookies;
    [self didChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldHandleCookies))];
}

- (void)setHTTPShouldUsePipelining:(BOOL)HTTPShouldUsePipelining {
    [self willChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldUsePipelining))];
    _HTTPShouldUsePipelining = HTTPShouldUsePipelining;
    [self didChangeValueForKey:NSStringFromSelector(@selector(HTTPShouldUsePipelining))];
}

- (void)setNetworkServiceType:(NSURLRequestNetworkServiceType)networkServiceType {
    [self willChangeValueForKey:NSStringFromSelector(@selector(networkServiceType))];
    _networkServiceType = networkServiceType;
    [self didChangeValueForKey:NSStringFromSelector(@selector(networkServiceType))];
}

- (void)setTimeoutInterval:(NSTimeInterval)timeoutInterval {
    [self willChangeValueForKey:NSStringFromSelector(@selector(timeoutInterval))];
    _timeoutInterval = timeoutInterval;
    [self didChangeValueForKey:NSStringFromSelector(@selector(timeoutInterval))];
}

3 AFHTTPRequestSerializer的各种请求头域处理方法

/**
 返回请求头域key和vaue

 @return 字典
 */
- (NSDictionary *)HTTPRequestHeaders {
    NSDictionary __block *value;
    dispatch_sync(self.requestHeaderModificationQueue, ^{
        value = [NSDictionary dictionaryWithDictionary:self.mutableHTTPRequestHeaders];
    });
    return value;
}

/**
 设置一个请求头域

 @param value vaue
 @param field 域名
 */
- (void)setValue:(NSString *)value
forHTTPHeaderField:(NSString *)field
{
    dispatch_barrier_async(self.requestHeaderModificationQueue, ^{
        [self.mutableHTTPRequestHeaders setValue:value forKey:field];
    });
}
/**
 返回指定请求头域的值

 @param field 域名
 @return 值
 */
- (NSString *)valueForHTTPHeaderField:(NSString *)field {
    NSString __block *value;
    dispatch_sync(self.requestHeaderModificationQueue, ^{
        value = [self.mutableHTTPRequestHeaders valueForKey:field];
    });
    return value;
}

/**
 设置Basic Authorization的用户名和密码。记住需要是base64编码格式的。
 @param username 用户
 @param password 密码
 */
- (void)setAuthorizationHeaderFieldWithUsername:(NSString *)username
                                       password:(NSString *)password
{
    NSData *basicAuthCredentials = [[NSString stringWithFormat:@"%@:%@", username, password] dataUsingEncoding:NSUTF8StringEncoding];
    NSString *base64AuthCredentials = [basicAuthCredentials base64EncodedStringWithOptions:(NSDataBase64EncodingOptions)0];
    [self setValue:[NSString stringWithFormat:@"Basic %@", base64AuthCredentials] forHTTPHeaderField:@"Authorization"];
}
/**
 移除Basic Authorization的请求头
 */
- (void)clearAuthorizationHeader {
    dispatch_barrier_async(self.requestHeaderModificationQueue, ^{
        [self.mutableHTTPRequestHeaders removeObjectForKey:@"Authorization"];
    });
}

4 AFHTTPRequestSerializer的各种创建NSMutableURLRequest的方法

通过下面这三种方法处理不同类型的request对象的初始化和参数序列化。

/**
 根据给定的url、方法名、参数构建一个request。

 @param method 方法名
 @param URLString url地址
 @param parameters 参数,根据不同的请求方法构建出不同的模式
 @param error 构建出错
 @return 返回一个非multipartForm请求
 */
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
                                 URLString:(NSString *)URLString
                                parameters:(id)parameters
                                     error:(NSError *__autoreleasing *)error
{
    NSParameterAssert(method);
    NSParameterAssert(URLString);
    NSURL *url = [NSURL URLWithString:URLString];
    NSParameterAssert(url);
    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
    mutableRequest.HTTPMethod = method;
    /*
     *mutableObservedChangedKeyPaths集合里面的属性都通过`setValue: forKey`手动设置一下。估计目的是触发这几个属性的kvo。
     */
    for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
        if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
            [mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
        }
    }
    /*
     根据parameters和HTTPRequestHeaders构建一个request
     */
    mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];
    return mutableRequest;
}

/**
 构建一个multipartForm的request。并且通过`AFMultipartFormData`类型的formData来构建请求体

 @param method 方法名,一般都是POST
 @param URLString 请求地址
 @param parameters 请求头参数
 @param block 用于构建请求体的Block
 @param error 构建请求体出错
 @return 返回一个构建好的request
 */
- (NSMutableURLRequest *)multipartFormRequestWithMethod:(NSString *)method
                                              URLString:(NSString *)URLString
                                             parameters:(NSDictionary *)parameters
                              constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
                                                  error:(NSError *__autoreleasing *)error
{
    NSParameterAssert(method);
    NSParameterAssert(![method isEqualToString:@"GET"] && ![method isEqualToString:@"HEAD"]);
    /*
     先构建一个普通的request对象,然后在构建出multipartFrom的request
     * 在这一步将会把parameters加入请求头或者请求体。然后把`AFURLRequestSerialization`指定的headers加入request的请求头中。这个request就只差构建multipartFrom部分了
     */
    NSMutableURLRequest *mutableRequest = [self requestWithMethod:method URLString:URLString parameters:nil error:error];
    /*
     *初始化一个`AFStreamingMultipartFormData`对象。用于封装multipartFrom的body部分
     */
    __block AFStreamingMultipartFormData *formData = [[AFStreamingMultipartFormData alloc] initWithURLRequest:mutableRequest stringEncoding:NSUTF8StringEncoding];
    if (parameters) {
        /*
         把parameters拼接成`AFQueryStringPair`对象。然后根据取出的key和value处理。
         */
        for (AFQueryStringPair *pair in AFQueryStringPairsFromDictionary(parameters)) {
            NSData *data = nil;
            //把value处理为NSData类型
            if ([pair.value isKindOfClass:[NSData class]]) {
                data = pair.value;
            } else if ([pair.value isEqual:[NSNull null]]) {
                data = [NSData data];
            } else {
                data = [[pair.value description] dataUsingEncoding:self.stringEncoding];
            }
            if (data) {
                [formData appendPartWithFormData:data name:[pair.field description]];
            }
        }
    }
    if (block) {
        block(formData);
    }
    //body具体序列化操作
    return [formData requestByFinalizingMultipartFormData];
}

/**
 通过一个Multipart-Form的request创建一个request。新request的httpBody是`fileURL`指定的文件。
 并且是通过`HTTPBodyStream`这个属性添加,`HTTPBodyStream`属性的数据会自动添加为httpBody。

 @param request 原request
 @param fileURL 文件的url
 @param handler 错误处理
 @return 处理完成的request
 */
- (NSMutableURLRequest *)requestWithMultipartFormRequest:(NSURLRequest *)request
                             writingStreamContentsToFile:(NSURL *)fileURL
                                       completionHandler:(void (^)(NSError *error))handler
{
    NSParameterAssert(request.HTTPBodyStream);
    NSParameterAssert([fileURL isFileURL]);
    //获取`HTTPBodyStream`属性
    NSInputStream *inputStream = request.HTTPBodyStream;
    //获取文件的数据流
    NSOutputStream *outputStream = [[NSOutputStream alloc] initWithURL:fileURL append:NO];
    __block NSError *error = nil;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //把读和写的操作加入当前线程的runloop
        [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
        //打开读和写数据流
        [inputStream open];
        [outputStream open];
        //循环做读和写操作
        while ([inputStream hasBytesAvailable] && [outputStream hasSpaceAvailable]) {
            uint8_t buffer[1024];

            NSInteger bytesRead = [inputStream read:buffer maxLength:1024];
            if (inputStream.streamError || bytesRead < 0) {
                error = inputStream.streamError;
                break;
            }

            NSInteger bytesWritten = [outputStream write:buffer maxLength:(NSUInteger)bytesRead];
            if (outputStream.streamError || bytesWritten < 0) {
                error = outputStream.streamError;
                break;
            }

            if (bytesRead == 0 && bytesWritten == 0) {
                break;
            }
        }
        //读和写完成。关闭读和写数据流
        [outputStream close];
        [inputStream close];
        //如果有handler,调用handler这个Block
        if (handler) {
            dispatch_async(dispatch_get_main_queue(), ^{
                handler(error);
            });
        }
    });
    //获取一个新的request,新的request的httpBody已经通过`HTTPBodyStream`转换成功
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    mutableRequest.HTTPBodyStream = nil;
    //返回一个request对象
    return mutableRequest;
}

3 AFStreamingMultipartFormData私有类的解析

首先,我们看几个全局方法。下面几个方法用于拼接multipart/form-data的分隔符和文件的MIMEType

/*
 生成multipartForm的request的boundary
 */
static NSString * AFCreateMultipartFormBoundary() {
    return [NSString stringWithFormat:@"Boundary+%08X%08X", arc4random(), arc4random()];
}
//回车换行符
static NSString * const kAFMultipartFormCRLF = @"\r\n";
//生成一个request的请求体中的参数的开始符号,第一个
static inline NSString * AFMultipartFormInitialBoundary(NSString *boundary) {
    return [NSString stringWithFormat:@"--%@%@", boundary, kAFMultipartFormCRLF];
}
//生成一个request的请求体中的参数的开始符号,菲第一个。
static inline NSString * AFMultipartFormEncapsulationBoundary(NSString *boundary) {
    return [NSString stringWithFormat:@"%@--%@%@", kAFMultipartFormCRLF, boundary, kAFMultipartFormCRLF];
}
//生成一个request的请求体中的参数的结束符号
static inline NSString * AFMultipartFormFinalBoundary(NSString *boundary) {
    return [NSString stringWithFormat:@"%@--%@--%@", kAFMultipartFormCRLF, boundary, kAFMultipartFormCRLF];
}
/*
根据文件的扩展名获取文件的`MIMEType`
 */
static inline NSString * AFContentTypeForPathExtension(NSString *extension) {
    NSString *UTI = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)extension, NULL);
    NSString *contentType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass((__bridge CFStringRef)UTI, kUTTagClassMIMEType);
    if (!contentType) {
        return @"application/octet-stream";
    } else {
        return contentType;
    }
}

AFStreamingMultipartFormData负责multipart/form-data的Body的具体构建。比如boundary的指定、请求体数据的拼接等。

- (instancetype)initWithURLRequest:(NSMutableURLRequest *)urlRequest
                    stringEncoding:(NSStringEncoding)encoding
{
    self = [super init];
    if (!self) {
        return nil;
    }
    //需要添加httpbody的request
    self.request = urlRequest;
    //字符编码
    self.stringEncoding = encoding;
    //指定boundary
    self.boundary = AFCreateMultipartFormBoundary();
    //这个属性用于存储httpbody数据
    self.bodyStream = [[AFMultipartBodyStream alloc] initWithStringEncoding:encoding];
    return self;
}
/*
 根据文件的url添加一个`multipart/form-data`请求的请求体域
 */
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
                         name:(NSString *)name
                        error:(NSError * __autoreleasing *)error
{
    NSParameterAssert(fileURL);
    NSParameterAssert(name);
    //文件扩展名
    NSString *fileName = [fileURL lastPathComponent];
    //获取文件的mimetype的类型
    NSString *mimeType = AFContentTypeForPathExtension([fileURL pathExtension]);

    return [self appendPartWithFileURL:fileURL name:name fileName:fileName mimeType:mimeType error:error];
}

/**
 根据指定类型的fileurl,把数据添加进入bodyStream中。以提供给后面构建request的body。

 @param fileURL 文件的url
 @param name 参数名称
 @param fileName 文件名称
 @param mimeType 文件类型
 @param error 错误
 @return 是否成功
 */
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
                         name:(NSString *)name
                     fileName:(NSString *)fileName
                     mimeType:(NSString *)mimeType
                        error:(NSError * __autoreleasing *)error
{
    NSParameterAssert(fileURL);
    NSParameterAssert(name);
    NSParameterAssert(fileName);
    NSParameterAssert(mimeType);
    /*
     各种错误情况判断
     */
    if (![fileURL isFileURL]) {
        NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"Expected URL to be a file URL", @"AFNetworking", nil)};
        if (error) {
            *error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo];
        }
        return NO;
    } else if ([fileURL checkResourceIsReachableAndReturnError:error] == NO) {
        NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"File URL not reachable.", @"AFNetworking", nil)};
        if (error) {
            *error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo];
        }
        return NO;
    }
    //获取指定路径文件的属性
    NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:[fileURL path] error:error];
    if (!fileAttributes) {
        return NO;
    }
    //添加`Content-Disposition`和`Content-Type`这两个请求体域
    NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
    [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"];
    [mutableHeaders setValue:mimeType forKey:@"Content-Type"];
    //把一个完整的请求体域封装进一个`AFHTTPBodyPart`对象中。
    AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
    bodyPart.stringEncoding = self.stringEncoding;
    bodyPart.headers = mutableHeaders;
    bodyPart.boundary = self.boundary;
    bodyPart.body = fileURL;
    bodyPart.bodyContentLength = [fileAttributes[NSFileSize] unsignedLongLongValue];
    [self.bodyStream appendHTTPBodyPart:bodyPart];

    return YES;
}
/**
 根据指定类型的数据流,把数据添加进入bodyStream中。以提供给后面构建request的body。
 
 @param inputStream 输入的数据流
 @param name 参数名称
 @param fileName 文件名称
 @param mimeType 文件类型
 */
- (void)appendPartWithInputStream:(NSInputStream *)inputStream
                             name:(NSString *)name
                         fileName:(NSString *)fileName
                           length:(int64_t)length
                         mimeType:(NSString *)mimeType
{
    NSParameterAssert(name);
    NSParameterAssert(fileName);
    NSParameterAssert(mimeType);
    //添加`Content-Disposition`和`Content-Type`这两个请求体域
    NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
    [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"];
    [mutableHeaders setValue:mimeType forKey:@"Content-Type"];
    //把一个完整的请求体域封装进一个`AFHTTPBodyPart`对象中
    AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
    bodyPart.stringEncoding = self.stringEncoding;
    bodyPart.headers = mutableHeaders;
    bodyPart.boundary = self.boundary;
    bodyPart.body = inputStream;
    bodyPart.bodyContentLength = (unsigned long long)length;
    [self.bodyStream appendHTTPBodyPart:bodyPart];
}

/**
 根据指定的data添加到请求体域中

 @param data 数据
 @param name 名称
 @param fileName 文件名称
 @param mimeType mimeType
 */
- (void)appendPartWithFileData:(NSData *)data
                          name:(NSString *)name
                      fileName:(NSString *)fileName
                      mimeType:(NSString *)mimeType
{
    NSParameterAssert(name);
    NSParameterAssert(fileName);
    NSParameterAssert(mimeType);

    NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
    [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"; filename=\"%@\"", name, fileName] forKey:@"Content-Disposition"];
    [mutableHeaders setValue:mimeType forKey:@"Content-Type"];
    
    [self appendPartWithHeaders:mutableHeaders body:data];
}

/**
 根据指定的key和value拼接到`Content-Disposition`属性中

 @param data 参数值
 @param name 参数名
 */
- (void)appendPartWithFormData:(NSData *)data
                          name:(NSString *)name
{
    NSParameterAssert(name);

    NSMutableDictionary *mutableHeaders = [NSMutableDictionary dictionary];
    [mutableHeaders setValue:[NSString stringWithFormat:@"form-data; name=\"%@\"", name] forKey:@"Content-Disposition"];
    //把处理好的数据加入对应的request的请求体中`Content-Disposition`部分
    [self appendPartWithHeaders:mutableHeaders body:data];
}

/**
 给一个multipartForm的`Content-Disposition`添加boundary

 @param headers 请求头域
 @param body 值
 */
- (void)appendPartWithHeaders:(NSDictionary *)headers
                         body:(NSData *)body
{
    NSParameterAssert(body);

    AFHTTPBodyPart *bodyPart = [[AFHTTPBodyPart alloc] init];
    bodyPart.stringEncoding = self.stringEncoding;
    bodyPart.headers = headers;
    bodyPart.boundary = self.boundary;
    bodyPart.bodyContentLength = [body length];
    bodyPart.body = body;

    [self.bodyStream appendHTTPBodyPart:bodyPart];
}

- (void)throttleBandwidthWithPacketSize:(NSUInteger)numberOfBytes
                                  delay:(NSTimeInterval)delay
{
    self.bodyStream.numberOfBytesInPacket = numberOfBytes;
    self.bodyStream.delay = delay;
}

/**
 根据一个request对应的`AFStreamingMultipartFormData`对象获取封装好的request对象

 @return multipart/form的request对象
 */
- (NSMutableURLRequest *)requestByFinalizingMultipartFormData {
    if ([self.bodyStream isEmpty]) {
        return self.request;
    }
    // Reset the initial and final boundaries to ensure correct Content-Length
    //重置boundary,从而确保`Content-Length`正确
    [self.bodyStream setInitialAndFinalBoundaries];
    //把拼接好的bodyStream添加进入request中
    [self.request setHTTPBodyStream:self.bodyStream];
    //给requst的请求头添加Content-Type属性指定为`multipart/form-data`类型的request。同时设置请求体的长度Content-Length。
    [self.request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@", self.boundary] forHTTPHeaderField:@"Content-Type"];
    [self.request setValue:[NSString stringWithFormat:@"%llu", [self.bodyStream contentLength]] forHTTPHeaderField:@"Content-Length"];
    return self.request;
}

4 AFJSONRequestSerializerAFPropertyListRequestSerializer

这两个类继承自AFHTTPRequestSerializer。他们的基本实现都是继承自父类。但是也根据自身不同情况,做了处理。

对于AFJSONRequestSerializer。需要把Content-Type指定为"application/json。同时HTTPBody
需要使用JSON序列化:

- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(id)parameters
                                        error:(NSError *__autoreleasing *)error
{
    NSParameterAssert(request);
    /*
     对于`GET`,`HEAD`,`DELETE`等方法中。直接使用父类的处理方式
     */
    if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
        return [super requestBySerializingRequest:request withParameters:parameters error:error];
    }
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    //把`HTTPRequestHeaders`中的值添加进入请求头中。
    [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
        if (![request valueForHTTPHeaderField:field]) {
            [mutableRequest setValue:value forHTTPHeaderField:field];
        }
    }];
    if (parameters) {
        //设置请求头的`Content-Type`类型
        if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
            [mutableRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
        }

        if (![NSJSONSerialization isValidJSONObject:parameters]) {
            if (error) {
                NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"The `parameters` argument is not valid JSON.", @"AFNetworking", nil)};
                *error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorCannotDecodeContentData userInfo:userInfo];
            }
            return nil;
        }
        //把parameters转换为JSON序列化的data
        NSData *jsonData = [NSJSONSerialization dataWithJSONObject:parameters options:self.writingOptions error:error];
        if (!jsonData) {
            return nil;
        }
        //JSON序列化的数据设置为httpbody
        [mutableRequest setHTTPBody:jsonData];
    }
    return mutableRequest;
}

对于AFPropertyListRequestSerializer也是同样的道理:

- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                               withParameters:(id)parameters
                                        error:(NSError *__autoreleasing *)error
{
    NSParameterAssert(request);
    /*
     对于`GET`,`HEAD`,`DELETE`等方法中。直接使用父类的处理方式
     */
    if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
        return [super requestBySerializingRequest:request withParameters:parameters error:error];
    }
    NSMutableURLRequest *mutableRequest = [request mutableCopy];
    //把`HTTPRequestHeaders`中的值添加进入请求头中。
    [self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
        if (![request valueForHTTPHeaderField:field]) {
            [mutableRequest setValue:value forHTTPHeaderField:field];
        }
    }];
    if (parameters) {
        //设置请求头的`Content-Type`类型
        if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
            [mutableRequest setValue:@"application/x-plist" forHTTPHeaderField:@"Content-Type"];
        }
        //把parameters转换为Plist序列化的data
        NSData *plistData = [NSPropertyListSerialization dataWithPropertyList:parameters format:self.format options:self.writeOptions error:error];
        if (!plistData) {
            return nil;
        }
        //Plist序列化的数据设置为httpbody
        [mutableRequest setHTTPBody:plistData];
    }
    return mutableRequest;
}

5 总结

这个类主要实现了对于不同情况的请求的request对象的封装。尤其是对于multipart/form-data类型的request的封装,简化了我们自己封装过程的痛苦。如果我们要使用multipart/form-data类型的请求。强烈推荐使用AFHTTPSessionManager对象的AFHTTPRequestSerialization来处理参数的序列化过程。下面就是使用AFHTTPRequestSerailization序列化和自己拼装的不同:

- (IBAction)updatePic:(id)sender {
    //请求头参数
    NSDictionary *dic = @{
                          @"businessType":@"CC_USER_CENTER",
                          @"fileType":@"image",
                          @"file":@"img.jpeg"
                          };
    //请求体图片数据
    NSData *imageData = UIImagePNGRepresentation([UIImage imageNamed:@"1.png"]);
    //创建request
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:[NSURL URLWithString:url]];
    //post方法
    [request setHTTPMethod:@"POST"];
    AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc]initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    NSURLSessionDataTask *task = [manager POST:url parameters:dic constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
        //请求体里面的参数
        NSDictionary *bodyDic = @{
                                  @"Content-Disposition":@"form-data;name=\"file\";filename=\"img.jpeg\"",
                                  @"Content-Type":@"image/png",
                                  };
        [formData appendPartWithHeaders:bodyDic body:imageData];
    } progress:^(NSProgress * _Nonnull uploadProgress) {
        NSLog(@"下载进度");
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"下载成功:%@",responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"下载失败%@",error);
    }];
    [task resume];
}
- (IBAction)multipartformPost3:(id)sender {
    //参数
    NSDictionary *dic = @{
                          @"businessType":@"CC_USER_CENTER",
                          @"fileType":@"image",
                          @"file":@"img.jpeg"
                          };
    NSString *boundaryString = @"xxxxx";
    NSMutableString *str = [NSMutableString string];
    [dic enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        [str appendFormat:@"--%@\r\n",boundaryString];
        [str appendFormat:@"%@name=\"%@\"\r\n\r\n",@"Content-Disposition: form-data;",key];
        [str appendFormat:@"%@\r\n",obj];
    }];
    
    NSMutableData *requestMutableData=[NSMutableData data];
    
    [str appendFormat:@"--%@\r\n",boundaryString];
    [str appendFormat:@"%@:%@",@"Content-Disposition",@"form-data;"];
    [str appendFormat:@"%@=\"%@\";",@"name",@"file"];
    [str appendFormat:@"%@=\"%@\"\r\n",@"filename",@"img1.jpeg"];
    [str appendFormat:@"%@:%@\r\n\r\n",@"Content-Type",@"image/png"];
    //转换成为二进制数据
    [requestMutableData appendData:[str dataUsingEncoding:NSUTF8StringEncoding]];
    NSData *imageData = UIImagePNGRepresentation([UIImage imageNamed:@"1.png"]);
    //文件数据部分
    [requestMutableData appendData:imageData];
    //添加结尾boundary
    [requestMutableData appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",boundaryString] dataUsingEncoding:NSUTF8StringEncoding]];

    
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:[NSURL URLWithString:url]];
    //post方法
    [request setHTTPMethod:@"POST"];
    // 设置请求头格式为Content-Type:multipart/form-data; boundary=xxxxx
    [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",boundaryString] forHTTPHeaderField:@"Content-Type"];
    request.HTTPBody = requestMutableData;
    
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSString *result = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"%@",result);
    }];
    
    [task resume];

}

最后原文地址,demo地址

查看原文

赞 0 收藏 3 评论 0

huang303513 发布了文章 · 2017-04-20

AFNetWorking源码之AFHTTPSessionManager

1 概述

AFHTTPSessionManagerAFURLSessionManager的子类。我们可以通过这个类做HTTP请求。其实整个AFHTTPSessionManager逻辑很简单,只是用HTTP的方式拼接了请求,并且调用父类的方式做处理。我会通过AFHTTPSessionManagerapi来讲一下POST上传数据的几种基本格式,然后我再随便分析一下AFHTTPSessionManager

2 POST请求的常用格式

HTTP/1.1协议规定的HTTP请求方法有OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT 这几种。其中POST一般用来向服务端提交数据,接下来要讨论POST提交数据的几种方式。协议规定POST提交的数据必须放在消息主体中,但协议并没有规定数据必须使用什么编码方式。实际上,开发者完全可以自己决定消息主体的格式,只要最后发送的 HTTP 请求满足上面的格式就可以。

但是,数据发送出去,还要服务端解析成功才有意义。一般服务端语言如php、python等,以及它们的framework,都内置了自动解析常见数据格式的功能。服务端通常是根据请求头(headers)中的Content-Type字段来获知请求中的消息主体是用何种方式编码,再对主体进行解析。所以说到POST提交数据方案,包含了Content-Type和消息主体编码方式两部分。

2.1 application/x-www-form-urlencoded格式的POST请求

这应该是最常见的 POST 提交数据的方式了。浏览器的原生表单,如果不设置enctype属性,那么最终就会以application/x-www-form-urlencoded方式提交数据。Content-Type被指定为application/x-www-form-urlencoded,提交的数据按照 key1=val1&key2=val2的方式进行编码,key和val都进行了URL转码。

下面这个请求是简书进入一篇文章页面的时候,会自动往服务器POST一个请求,估计是统计文章被阅读的次数等功能。具体看下面:

//发送的请求,删除了cookie相关的部分
POST /notes/e15592ce40ae/mark_viewed.json HTTP/1.1
Host: www.jianshu.com
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:52.0) Gecko/20100101 Firefox/52.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
X-CSRF-Token: vJvptva4Tqou/V3dd3nFCrcvRsb78FReHuIYZke5PVAnfR/tIAAMCfuaB2Z2/gaEohIZAsiEksUYyPqzg3DpSA==
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: http://www.jianshu.com/p/e15592ce40ae
Content-Length: 98
Connection: keep-alive
Cache-Control: max-age=0
//请求体

uuid=4e3abc0f-1824-4a5d-982f-7d9dee92d9cd&referrer=http%3A%2F%2Fwww.jianshu.com%2Fu%2Fad726ba6935d

AFHTTPSessionManager实现上面这个application/x-www-form-urlencoded请求。

    AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc]initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    NSDictionary *params = @{
                             @"uuid":@"4e3abc0f-1824-4a5d-982f-7d9dee92d9cd",
                             @"referrer":@"http://www.jianshu.com/p/e15592ce40ae"
                             };
    NSURLSessionDataTask *task = [manager POST:@"http://www.jianshu.com//notes/e15592ce40ae/mark_viewed.json" parameters:params progress:^(NSProgress * _Nonnull uploadProgress) {
        NSLog(@"进度更新");
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"返回数据:%@",responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"返回错误:%@",error);
    }];
    [task resume];

2.2 multipart/form-data格式的POST请求

Multipart/form-data的基础方法是POST , 也就是说是由POST方法来组合实现的.
Multipart/form-data与POST方法的不同之处在于请求头和请求体.
Multipart/form-data的请求头必须包含一个特殊的头信息 : Content-Type , 且其值也必须规定为multipart/form-data , 同时还需要规定一个内容分割符用于分割请求体中的多个POST的内容 , 如文件内容和文本内容自然需要分割开来 , 不然接收方就无法正常解析和还原这个文件了.
Multipart/form-data的请求体也是一个字符串 , 不过和post的请求体不同的是它的构造方式 , post是简单的name=value值连接 , 而Multipart/form-data则是添加了分隔符等内容的构造体.

请求的头部信息如下:

//其中xxxxx是我自定义的分隔符,每个人都可以选择自己的分隔符
Content-Type: multipart/form-data; boundary=xxxxx

下面我们来看一下一个我的Multipart/form-data请求体:

POST /uploadFile HTTP/1.1
Host: 这里是url,就不暴露了^_^
Content-Type: multipart/form-data; boundary=xxxxx
Connection: keep-alive
Accept: */*
User-Agent: AFNetWorking3.X%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/1 CFNetwork/808.2.16 Darwin/15.6.0
Content-Length: 32175
Accept-Language: en-us
Accept-Encoding: gzip, deflate

--xxxxx
Content-Disposition: form-data;name="file"

img.jpeg
--xxxxx
Content-Disposition: form-data;name="businessType"

CC_USER_CENTER
--xxxxx
Content-Disposition: form-data;name="fileType"

image
--xxxxx
Content-Disposition:form-data;name="file";filename="img1.jpeg"
Content-Type:image/png

这里是图片数据,太长了.我就删了

--xxxxx--

这个请求有三个参数file,businessType,fileType。比如file参数和他的值就通过如下格式传输:

--xxxxx
Content-Disposition: form-data;name="file"

img.jpeg

上面这种就是一个参数与之对应的值。协议规定的就是这个格式,没有为什么。我们可以看看图片数据部分:

--xxxxx
Content-Disposition:form-data;name="file";filename="img1.jpeg"
Content-Type:image/png

这里是图片数据,太长了.我就删了

--xxxxx--

其中name="参数名" filename="文件名" 其中参数名这个要和接收方那边相对应 正常开发中可以去问服务器那边 , 文件名是说在服务器端保存成文件的名字 , 这个参数然并卵 , 因为一般服务端会按照他们自己的要求去处理文件的存储.

下一行是指定类型 , 我这里示例中写的是PNG图片类型 , 这个可以根据你的实际需求的写。如果我们要上传多分图片或者文件,则只需要按照指定格式就可以了,比如下面就是上传两张图片的请求:

POST /uploadFile HTTP/1.1
Host: 这里是url,就不暴露了^_^
Content-Type: multipart/form-data; boundary=xxxxx
Connection: keep-alive
Accept: */*
User-Agent: AFNetWorking3.X%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/1 CFNetwork/808.2.16 Darwin/15.6.0
Content-Length: 32175
Accept-Language: en-us
Accept-Encoding: gzip, deflate

--xxxxx
Content-Disposition: form-data;name="file"

img.jpeg
--xxxxx
Content-Disposition: form-data;name="businessType"

CC_USER_CENTER
--xxxxx
Content-Disposition: form-data;name="fileType"

image
--xxxxx
Content-Disposition:form-data;name="file";filename="img1.jpeg"
Content-Type:image/png

这里是图片1数据,太长了.我就删了
--xxxxx
Content-Disposition:form-data;name="file";filename="img2.jpeg"
Content-Type:image/png

这里是图片1数据,太长了.我就删了
--xxxxx--

下面是我Demo中一个multipart/form-data请求的实现代码,分别用NSRULDataTaskAFHTTPSessionManager实现,我们可以发现用第二种方法简便了很多,因为AFN已经帮我们做好了拼接工作:

//方法一
- (IBAction)updatePic:(id)sender {
    //请求头参数
    NSDictionary *dic = @{
                          @"businessType":@"CC_USER_CENTER",
                          @"fileType":@"image",
                          @"file":@"img.jpeg"
                          };
    //请求体图片数据
    NSData *imageData = UIImagePNGRepresentation([UIImage imageNamed:@"1.png"]);
    //创建request
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:[NSURL URLWithString:url]];
    //post方法
    [request setHTTPMethod:@"POST"];
    // 设置请求头格式为Content-Type:multipart/form-data; boundary=xxxxx
    //[request setValue:@"multipart/form-data; boundary=xxxxx" forHTTPHeaderField:@"Content-Type"];
    AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc]initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    NSURLSessionDataTask *task = [manager POST:url parameters:dic constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
        //请求体里面的参数
        NSDictionary *bodyDic = @{
                                  @"Content-Disposition":@"form-data;name=\"file\";filename=\"img.jpeg\"",
                                  @"Content-Type":@"image/png",
                                  };
        [formData appendPartWithHeaders:bodyDic body:imageData];
    } progress:^(NSProgress * _Nonnull uploadProgress) {
        NSLog(@"下载进度");
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"下载成功:%@",responseObject);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"下载失败%@",error);
    }];
    [task resume];
}
//方法二
- (IBAction)multipartformPost2:(id)sender {
    //参数
    NSDictionary *dic = @{
                          @"businessType":@"CC_USER_CENTER",
                          @"fileType":@"image",
                          @"file":@"img.jpeg"
                          };
    NSString *boundaryString = @"xxxxx";
    NSMutableString *str = [NSMutableString string];
    [dic enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        [str appendFormat:@"--%@\r\n",boundaryString];
        [str appendFormat:@"%@name=\"%@\"\r\n\r\n",@"Content-Disposition: form-data;",key];
        [str appendFormat:@"%@\r\n",obj];
    }];
    
     NSMutableData *requestMutableData=[NSMutableData data];

    [str appendFormat:@"--%@\r\n",boundaryString];
    [str appendFormat:@"%@:%@",@"Content-Disposition",@"form-data;"];
    [str appendFormat:@"%@=\"%@\";",@"name",@"file"];
    [str appendFormat:@"%@=\"%@\"\r\n",@"filename",@"img1.jpeg"];
    [str appendFormat:@"%@:%@\r\n\r\n",@"Content-Type",@"image/png"];
    //转换成为二进制数据
    [requestMutableData appendData:[str dataUsingEncoding:NSUTF8StringEncoding]];
    NSData *imageData = UIImagePNGRepresentation([UIImage imageNamed:@"1.png"]);
    //文件数据部分
    [requestMutableData appendData:imageData];
    //添加结尾boundary
    [requestMutableData appendData:[[NSString stringWithFormat:@"\r\n--%@--\r\n",boundaryString] dataUsingEncoding:NSUTF8StringEncoding]];
    //创建一个请求对象
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc]initWithURL:[NSURL URLWithString:url]];
    //post方法
    [request setHTTPMethod:@"POST"];
    // 设置请求头格式为Content-Type:multipart/form-data; boundary=xxxxx
    [request setValue:[NSString stringWithFormat:@"multipart/form-data; boundary=%@",boundaryString] forHTTPHeaderField:@"Content-Type"];
    //session
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    NSURLSessionDataTask *task = [session uploadTaskWithRequest:request fromData:requestMutableData completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSString *result = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"%@",result);
    }];
    [task resume];
}

Multipart/form-data格式的POST请求总结:

  • 文件类型参数中name="参数名"一定要和服务端对应, 开发的时候 , 可以问服务端人员,我这里是file

  • 上传文件的数据部分使用二进制数据(NSData)拼接。

  • 上边界部分和下边界部分的字符串 , 最后都要转换成二进制数据(NSData) , 和文件部分的二进制数据拼接在一起 , 作为请求体发送给服务器。

  • 每一行末尾需要有一定的`rn·。

2.3 application/json格式的POST请求

接下来我将常使用NSURLSessionDataTask做一个application/json的POST请求。并且请求体数据我存储在一个test.txt文件中,从文件中读取出来然后上传。

//test.txt文件内容
{"name":"huang","phone":"124"}

通过抓包软件我的请求如下,和其他POST请求原理一样,只是拼接请求体的方式不一样,并且更具不同格式的请求体,设置不同的Content-Type

POST /posts HTTP/1.1
Host: jsonplaceholder.typicode.com
Content-Type: application/json
Connection: keep-alive
Accept: application/json
User-Agent: AFNetWorking3.X%E6%BA%90%E7%A0%81%E9%98%85%E8%AF%BB/1 CFNetwork/808.2.16 Darwin/15.6.0
Content-Length: 31
Accept-Language: en-us
Accept-Encoding: gzip, deflate

{"name":"huang","phone":"124"}

下面是我Demo的具体实现

- (IBAction)applicationjsonPOST2:(id)sender {
    NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://jsonplaceholder.typicode.com/posts"]];
    //指请求体的类型。由于我们test.txt里面的文件是json格式的字符串。所以我这里指定为`application/json`
    [request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [request addValue:@"application/json" forHTTPHeaderField:@"Accept"];
    [request setHTTPMethod:@"POST"];
    [request setCachePolicy:NSURLRequestReloadIgnoringCacheData];
    [request setTimeoutInterval:20];
    NSString *path = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"txt"];
    NSURL *url = [NSURL URLWithString:[path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    //使用Block来处理返回数据
    NSURLSessionDataTask *task = [session uploadTaskWithRequest:request fromFile:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSString *result = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"%@",result);
    }];
    [task resume];
}

AFHTTPSessionManager分析

上面主要讲了对POST请求的分析,主要是AFHTTPSessionManager并没有多少逻辑,他主要是调用AFURLSessionManager的实现。另外就是通过baseURL改变了url的拼接过程。下面我就抽出他们的不同点分析一下:

1 首先多了一个属性

@property (nonatomic, strong) AFHTTPRequestSerializer <AFURLRequestSerialization> * requestSerializer;

这个属性的主要作用就是帮我们拼接请求头和请求体,从上面的Demo我们发现很多请求的拼接工作都通过requestSerializer处理了。如果我们不手动设置,默认是一个AFHTTPRequestSerializer对象。具体可以去初始化方法里面看到。

2 重写了securityPolicy这个属性的setter方法,增加对于SSLPinningMode的异常处理。

- (void)setSecurityPolicy:(AFSecurityPolicy *)securityPolicy {
    //增加对于SSLPinningMode的异常处理。
    if (securityPolicy.SSLPinningMode != AFSSLPinningModeNone && ![self.baseURL.scheme isEqualToString:@"https"]) {
        NSString *pinningMode = @"Unknown Pinning Mode";
        switch (securityPolicy.SSLPinningMode) {
            case AFSSLPinningModeNone:        pinningMode = @"AFSSLPinningModeNone"; break;
            case AFSSLPinningModeCertificate: pinningMode = @"AFSSLPinningModeCertificate"; break;
            case AFSSLPinningModePublicKey:   pinningMode = @"AFSSLPinningModePublicKey"; break;
        }
        NSString *reason = [NSString stringWithFormat:@"A security policy configured with `%@` can only be applied on a manager with a secure base URL (i.e. https)", pinningMode];
        @throw [NSException exceptionWithName:@"Invalid Security Policy" reason:reason userInfo:nil];
    }
    //调用`AFURLSessionManager`的`securityPolicy`属性的setter方法。
    [super setSecurityPolicy:securityPolicy];
}

3 NSCopying和NSSecureCoding协议的实现过程

NSCopyingNSSecureCoding协议的实现过程添加了对requestSerializer,responseSerializer,securityPolicy这三个属性的复制。也就是说,用copy方法复制的manager,这三个属性的配置跟着一起复制。而父类AFURSSessionManager只实现了对configuration的复制。

+ (BOOL)supportsSecureCoding {
    return YES;
}
- (instancetype)initWithCoder:(NSCoder *)decoder {
    NSURL *baseURL = [decoder decodeObjectOfClass:[NSURL class] forKey:NSStringFromSelector(@selector(baseURL))];
    //获取当前manager的NSURLSessionConfiguration
    NSURLSessionConfiguration *configuration = [decoder decodeObjectOfClass:[NSURLSessionConfiguration class] forKey:@"sessionConfiguration"];
    if (!configuration) {
        NSString *configurationIdentifier = [decoder decodeObjectOfClass:[NSString class] forKey:@"identifier"];
        if (configurationIdentifier) {
        //iOS7和iOS8初始化NSURLSessionConfiguration方法不一样。所以要分开处理
#if (defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED >= 1100)
            configuration = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:configurationIdentifier];
#else
            configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:configurationIdentifier];
#endif
        }
    }
    //初始化一个新的manager
    self = [self initWithBaseURL:baseURL sessionConfiguration:configuration];
    if (!self) {
        return nil;
    }
    //添加了对`requestSerializer`,`responseSerializer`,`securityPolicy`这三个属性的接档。
    self.requestSerializer = [decoder decodeObjectOfClass:[AFHTTPRequestSerializer class] forKey:NSStringFromSelector(@selector(requestSerializer))];
    self.responseSerializer = [decoder decodeObjectOfClass:[AFHTTPResponseSerializer class] forKey:NSStringFromSelector(@selector(responseSerializer))];
    AFSecurityPolicy *decodedPolicy = [decoder decodeObjectOfClass:[AFSecurityPolicy class] forKey:NSStringFromSelector(@selector(securityPolicy))];
    if (decodedPolicy) {
        self.securityPolicy = decodedPolicy;
    }
    return self;
}

- (void)encodeWithCoder:(NSCoder *)coder {
    [super encodeWithCoder:coder];
    //添加对baseURL属性的归档
    [coder encodeObject:self.baseURL forKey:NSStringFromSelector(@selector(baseURL))];
    if ([self.session.configuration conformsToProtocol:@protocol(NSCoding)]) {
        [coder encodeObject:self.session.configuration forKey:@"sessionConfiguration"];
    } else {
        [coder encodeObject:self.session.configuration.identifier forKey:@"identifier"];
    }
    //添加了对`requestSerializer`,`responseSerializer`,`securityPolicy`这三个属性的归档。
    [coder encodeObject:self.requestSerializer forKey:NSStringFromSelector(@selector(requestSerializer))];
    [coder encodeObject:self.responseSerializer forKey:NSStringFromSelector(@selector(responseSerializer))];
    [coder encodeObject:self.securityPolicy forKey:NSStringFromSelector(@selector(securityPolicy))];
}

#pragma mark - NSCopying
- (instancetype)copyWithZone:(NSZone *)zone {
    AFHTTPSessionManager *HTTPClient = [[[self class] allocWithZone:zone] initWithBaseURL:self.baseURL sessionConfiguration:self.session.configuration];
    //添加了对`requestSerializer`,`responseSerializer`,`securityPolicy`这三个属性的复制。
    HTTPClient.requestSerializer = [self.requestSerializer copyWithZone:zone];
    HTTPClient.responseSerializer = [self.responseSerializer copyWithZone:zone];
    HTTPClient.securityPolicy = [self.securityPolicy copyWithZone:zone];
    return HTTPClient;
}

4 HEAD和PUT等方法的实现

我在这里不准备深入讲这两个方法是如何实现的,因为AFHTTPSessionManager主要通过他的requestSerializer属性来实现对HEADPUT等请求的拼接。我准备分析AFHTTPRequestSerializer的时候再看这一块是如何实现的。

 //通过requestSerializer属性来拼接request对象。
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:method URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:&serializationError];

最后原文地址,demo地址

查看原文

赞 0 收藏 3 评论 0

huang303513 发布了文章 · 2017-04-19

AFNetWorking源码之AFURLSessionManager

1 概述

AFNetWorking基本上是所有iOS项目的标配。现在升级带最新版的3.X了。得益于苹果从NSURLConnection升级到NSURLSession,AFN也实现了api的简化,同时功能却一点没少。我们来看一下AFN3.X的目录结构:

  • AFNetWorking 这个文件是一个头文件。啥也没做,就是引入了其他文件方便使用。

  • AFURLSessionManager 这个文件是核心类,基本上通过它来实现了大部分核心功能。负责请求的建立、管理、销毁、安全、请求重定向、请求重启等各种功能。他主要实现了NSURLSessionNSRULSessionTask的封装。

  • AFHTTPSessionManager 这个文件是AFURLSessionManager的子类。主要实现了对HTTP请求的优化。

  • AFURLRequestSerialization 这个主要用于请求头的编码解码、序列化、优化处理、简化请求拼接过程等。

  • AFURLResponseSerialization 这个主要用于网络返回数据的序列化、编码解码、序列化、数据处理等。

  • AFSecurityPolicy 这个主要用于请求的认证功能。比如https的认证模式等。

  • AFNetworkReachabilityManager 这个主要用于监听网络请求状态变化功能。

首先说明,看AFN源码之前一定要搞清楚NSURLSession系列的api,这样能让你事半功倍,具体可以看AFNetWorking源码之NSRULSession系列概述。在这篇文章里,我们主要讲解AFURLSessionManager的实现原理和封装过程。首先我们通过一个简单的网络请求看一下他的基本用法(大部分都是非必须的,这里为了掩饰写出来):

- (IBAction)clickButton:(id)sender {
    //通过默认配置初始化Session
    NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];
    //设置网络请求序列化对象
    AFHTTPRequestSerializer *requestSerializer = [AFHTTPRequestSerializer serializer];
    [requestSerializer setValue:@"test" forHTTPHeaderField:@"requestHeader"];
    requestSerializer.timeoutInterval = 60;
    requestSerializer.stringEncoding = NSUTF8StringEncoding;
    //设置返回数据序列化对象
    AFHTTPResponseSerializer *responseSerializer = [AFHTTPResponseSerializer serializer];
    manager.responseSerializer = responseSerializer;
    //网络请求安全策略
    if (true) {
        AFSecurityPolicy *securityPolicy;
        securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
        securityPolicy.allowInvalidCertificates = false;
        securityPolicy.validatesDomainName = YES;
        manager.securityPolicy = securityPolicy;
    } else {
        manager.securityPolicy.allowInvalidCertificates = true;
        manager.securityPolicy.validatesDomainName = false;
    }
    //是否允许请求重定向
    if (true) {
        [manager setTaskWillPerformHTTPRedirectionBlock:^NSURLRequest *(NSURLSession *session, NSURLSessionTask *task, NSURLResponse *response, NSURLRequest *request) {
            if (response) {
                return nil;
            }
            return request;
        }];
    }
    //监听网络状态
    [manager.reachabilityManager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
        NSLog(@"%ld",(long)status);
    }];
    [manager.reachabilityManager startMonitoring];
    
    NSURL *URL = [NSURL URLWithString:bigPic];
    NSURLRequest *request = [NSURLRequest requestWithURL:URL];
    NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:^(NSProgress *downloadProgress){
        NSLog(@"下载进度:%lld",downloadProgress.completedUnitCount);
    } destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
        NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
        NSURL *fileURL = [documentsDirectoryURL URLByAppendingPathComponent:[response suggestedFilename]];
        NSLog(@"fileURL:%@",[fileURL absoluteString]);
        return fileURL;
    } completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
        self.imageView.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:filePath]];
        NSLog(@"File downloaded to: %@", filePath);
    }];
    [downloadTask resume];
}

通过这个请求,我们发现AFURLSessionManager要负责以下几块功能。

  • 初始化和管理NSURLSession,通过它来建立和管理各种Task。

  • 初始化和管理NSRULSessionTask,通过不同task来发送不同请求。

  • 管理各种认证功能、安全功能、请求重定向、数据处理。

  • 管理和组织每个task的各种状态管理和通知管理。不同task的回调处理。

  • 帮我们管理和处理了NSRULSession系列api的各种代理方法。简化了我们的处理。

2 AFURLSessionManager的声明分析

AFURLSessionManager根据一个指定的NSURLSessionConfiguration创建和管理一个NSURLSession对象。并且这个对象实现了<NSURLSessionTaskDelegate>, <NSURLSessionDataDelegate>, <NSURLSessionDownloadDelegate>, 和 <NSURLSessionDelegate>这几个协议的协议方法。同时实现NSSecureCodingNSCopying来实现归档解档和copy功能。

2.1 AFURLSessionManager的初始化api

这些api主要用于初始化、安全策略、网络状态监听等:

interface AFURLSessionManager : NSObject <NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDataDelegate, NSURLSessionDownloadDelegate, NSSecureCoding, NSCopying>
//指定的初始化方法、通过他来初始化一个Manager对象。
- (instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)configuration 
//AFURLSessionManager通过session来管理和创建网络请求。一个manager就实现了对这个session的管理,他们是一一对应的关系。
@property (readonly, nonatomic, strong) NSURLSession *session;
//处理网络请求回调的操作队列,就是我们初始化session的时候传入的那个OperationQueue参数。如果不传入,默认是MainOperationQueue。
@property (readonly, nonatomic, strong) NSOperationQueue *operationQueue;
//对返回数据的处理都通过这个属性来处理,比如数据的提取、转换等。默认是一个`AFJSONResponseSerializer`对象用JSON的方式解析。
@property (nonatomic, strong) id <AFURLResponseSerialization> responseSerializer;
//用于指定session的安全策略。用于处理信任主机和证书认证等。默认是`defaultPolicy`。
@property (nonatomic, strong) AFSecurityPolicy *securityPolicy;
//观测网络状态的变化,具体可以看我的Demo用法。
@property (readwrite, nonatomic, strong) AFNetworkReachabilityManager *reachabilityManager;
@end

2.2 AFURLSessionManager获取Task的api

这部分api主要是任务的创建、任务的分类、任务完成队列处理、特殊情况的任务重新创建等:

//当前session创建的所有Task,这个是下面三种task的总和。
@property (readonly, nonatomic, strong) NSArray <NSURLSessionTask *> *tasks;
//当前session创建的DataTask
@property (readonly, nonatomic, strong) NSArray <NSURLSessionDataTask *> *dataTasks;
//当前session创建的uploadTask
@property (readonly, nonatomic, strong) NSArray <NSURLSessionUploadTask *> *uploadTasks;
//当前session创建的downloadTask
@property (readonly, nonatomic, strong) NSArray <NSURLSessionDownloadTask *> *downloadTasks;

//用于处理任务回调的GCD对象,默认是dispatch_main_queue。
@property (nonatomic, strong, nullable) dispatch_queue_t completionQueue;
//用于处理任务回调的GCD的group对象,如果不初始化、则一个默认的Group被使用。
@property (nonatomic, strong, nullable) dispatch_group_t completionGroup;
//在iOS7的环境下,我们通过background模式的session创建的uploadTask有时会是nil,如果这个属性是yes,AFN会尝试再次创建uploadTask。
@property (nonatomic, assign) BOOL attemptsToRecreateUploadTasksForBackgroundSessions;
//废除manager对应的Session。通过传入的参数来决定是否立即取消已经用session发出去的任务。
- (void)invalidateSessionCancelingTasks:(BOOL)cancelPendingTasks;

2.3 AFURLSessionManager为管理Task创建Block

AFURLSessionManager提供了很多创建Task的api。并且提供了很多处理Task的Block。应该说着几个api就是AFN为我们提供的最大价值,他把所有delegate方法细节都处理好。直接提供给我们一些最实用的api,我们就不用去管理session系列繁琐的delegate方法了。

//创建一个NSURLSessionDataTask
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
                            completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject,  NSError * _Nullable error))completionHandler;
//创建一个NSURLSessionDataTask,并且能获取上传或者下载进度
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
                               uploadProgress:(nullable void (^)(NSProgress *uploadProgress))uploadProgressBlock
                             downloadProgress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
                            completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject,  NSError * _Nullable error))completionHandler;

//创建一个上传Task,并且指定上传文件的路径。
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request
                                         fromFile:(NSURL *)fileURL
                                         progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgressBlock
                                completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError  * _Nullable error))completionHandler;
////创建一个上传Task,并且指定上传的数据。
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request
                                         fromData:(nullable NSData *)bodyData
                                         progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgressBlock
                                completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler;
//创建一个uploadTask,然后上传数据
- (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request
                                                 progress:(nullable void (^)(NSProgress *uploadProgress))uploadProgressBlock
                                        completionHandler:(nullable void (^)(NSURLResponse *response, id _Nullable responseObject, NSError * _Nullable error))completionHandler;
//新建一个download任务,destination表示的下载文件的缓存路径
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request
                                             progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
                                          destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                    completionHandler:(nullable void (^)(NSURLResponse *response, NSURL * _Nullable filePath, NSError * _Nullable error))completionHandler;
//继续恢复一个download任务。resumeData参数表示的是恢复下载的时候初始化数据,比如前面已经下载好的部分数据。
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData
                                                progress:(nullable void (^)(NSProgress *downloadProgress))downloadProgressBlock
                                             destination:(nullable NSURL * (^)(NSURL *targetPath, NSURLResponse *response))destination
                                       completionHandler:(nullable void (^)(NSURLResponse *response, NSURL * _Nullable filePath, NSError * _Nullable error))completionHandler;
//获取指定Task的上传进度
- (nullable NSProgress *)uploadProgressForTask:(NSURLSessionTask *)task;
//获取指定Task的下载进度
- (nullable NSProgress *)downloadProgressForTask:(NSURLSessionTask *)task;

注意:上面所有Task的progress都不在主线程、所以要在progress中做UI更新,都必须手动在主线程操作。

2.4 AFURLSessionManager设置各种情况的代理回调

这些回调Block主要是用于处理网络请求过程或者结束以后的数据处理、认证、通知、缓存等。我们可以通过设置这些Block来获取或者检测各种状态。相当于就是钩子函数。通过下面的这些Block,我们基本可以获取请求过程中的所有状态以及需要做的各种处理。

//设置Session出错或者无效的手的回调Block。这个Block主要在`NSURLSessionDelegate`代理的`URLSession:didBecomeInvalidWithError:`方法中执行。
- (void)setSessionDidBecomeInvalidBlock:(nullable void (^)(NSURLSession *session, NSError *error))block{
    
}
//当网络请需要的认证信息比如用户名密码已经发送了的时候,就可以通过这个Block来处理。这个Block是在`NSURLSessionDelegate`代理里面的`URLSession:didReceiveChallenge:completionHandler:`方法中被执行。注意这个是针对Session
- (void)setSessionDidReceiveAuthenticationChallengeBlock:(nullable NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * _Nullable __autoreleasing * _Nullable credential))block{
    
}
////当网络请需要的认证信息比如用户名密码已经发送了的时候,就可以通过这个Block来处理。这个Block是在`NSURLSessionTaskDelegate`代理里面的`URLSession:task:didReceiveChallenge:completionHandler:`方法中被执行。注意这个是针对Task。
- (void)setTaskDidReceiveAuthenticationChallengeBlock:(nullable NSURLSessionAuthChallengeDisposition (^)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential * _Nullable __autoreleasing * _Nullable credential))block{
    
}
//当请求需要一个新的bodystream的时候,就可以通过这个Block来设置。这个Block在`NSURLSessionTaskDelegate` 代理协议的`URLSession:task:needNewBodyStream:`方法里面设置。
- (void)setTaskNeedNewBodyStreamBlock:(nullable NSInputStream * (^)(NSURLSession *session, NSURLSessionTask *task))block{
    
}
//当一个网络请求需要重定向的时候。就会调用这个Block。这个Block是在`NSURLSessionTaskDelegate`协议的`URLSession:willPerformHTTPRedirection:newRequest:completionHandler:`方法中调用的。
- (void)setTaskWillPerformHTTPRedirectionBlock:(nullable NSURLRequest * (^)(NSURLSession *session, NSURLSessionTask *task, NSURLResponse *response, NSURLRequest *request))block{
    
}
//可以通过设置这个Block来获取上传进度。这个Block主要在`NSURLSessionTaskDelegate`协议的 `URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend:`方法中调用.
- (void)setTaskDidSendBodyDataBlock:(nullable void (^)(NSURLSession *session, NSURLSessionTask *task, int64_t bytesSent, int64_t totalBytesSent, int64_t totalBytesExpectedToSend))block{
    
}
//设置一个Task完成以后执行的Block,这个Block在`NSURLSessionTaskDelegate`协议的 `URLSession:task:didCompleteWithError:`方法中执行。
- (void)setTaskDidCompleteBlock:(nullable void (^)(NSURLSession *session, NSURLSessionTask *task, NSError * _Nullable error))block{
    
}
//当接收到网络请求返回以后,可以调用这个Block。这个Block是在`NSURLSessionDataDelegate`协议的 `URLSession:dataTask:didReceiveResponse:completionHandler:`
- (void)setDataTaskDidReceiveResponseBlock:(nullable NSURLSessionResponseDisposition (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLResponse *response))block{
    
}
//如果一个dataTask转换为downLoadTask以后,就可以设置这个Block来调用。在`NSURLSessionDataDelegate` 协议的`URLSession:dataTask:didBecomeDownloadTask:`方法中调用。
- (void)setDataTaskDidBecomeDownloadTaskBlock:(nullable void (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSURLSessionDownloadTask *downloadTask))block{
    
}
//当dataTask接收到数据以后,可以设置调用这个Block。具体在`NSURLSessionDataDelegate`协议的`URLSession:dataTask:didReceiveData:`方法。
- (void)setDataTaskDidReceiveDataBlock:(nullable void (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSData *data))block{
    
}
//设置一个Block来决定是否处理或者换成网络请求缓存。具体在`NSURLSessionDataDelegate`协议的`URLSession:dataTask:willCacheResponse:completionHandler:`方法中。
- (void)setDataTaskWillCacheResponseBlock:(nullable NSCachedURLResponse * (^)(NSURLSession *session, NSURLSessionDataTask *dataTask, NSCachedURLResponse *proposedResponse))block{
    
}
//当session所有的任务都发送出去以后,就可以通过这个Block来获取。具体在`NSURLSessionDataDelegate`协议的 `URLSessionDidFinishEventsForBackgroundURLSession:`方法中。
- (void)setDidFinishEventsForBackgroundURLSessionBlock:(nullable void (^)(NSURLSession *session))block{
    
}
//当一个downloadTask执行完毕以后,可以通过这个Block来获取下载信息,我们可以通过这个Block获取下载文件的位置。具体在`NSURLSessionDownloadDelegate`协议的`URLSession:downloadTask:didFinishDownloadingToURL:`方法中被调用。
- (void)setDownloadTaskDidFinishDownloadingBlock:(nullable NSURL * _Nullable  (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, NSURL *location))block{
    
}
//可以通过这个Block获取一个downloadTask的下载进度。这个Block会在下载过程中多次被调用。具体是在`NSURLSessionDownloadDelegate`协议中的`URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesWritten:totalBytesExpectedToWrite:`方法中被调用。
- (void)setDownloadTaskDidWriteDataBlock:(nullable void (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t bytesWritten, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite))block{
    
}
//当一个downloadTask重新开始以后,我们可以通过这个Block获取fileOffSet等信息获取已经下载的部分以及总共有多少要下载。具体是在`NSURLSessionDownloadDelegate`协议的`URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:`方法中被调用。
- (void)setDownloadTaskDidResumeBlock:(nullable void (^)(NSURLSession *session, NSURLSessionDownloadTask *downloadTask, int64_t fileOffset, int64_t expectedTotalBytes))block{
    
}

除了上面的部分,AFURLSessionManager的头文件还提供了很多notification的声明。通过这些通知,我们可以获取Task是否开始、是否完成、是否挂起、是否无效等各种通知。具体可以去文件里看。

3 AFURLSessionManager的实现分析

AFURLSessionManager.m文件里面除了有AFURLSessionManager.h定义的各种接口的实现意外,还有处理不同iOS版本下NSRULSession不同的部分,以及多个全局dispatch_queue_t的定义、以及处理NSURLSeesionTash的各种代理方法的实现和处理。具体划分如下:

  • NSURLSessionManager的实现。主要实现了接口文件定义的各种api的实现,比如Task的创建、Task的获取、Task的各种代理方法的实现、NSCoping和NSCoding协议、以及各种Block的实现。

    • 基本属性的初始化。比如sessionConfigurationoperationQueuesessionmutableTaskDelegatesKeyedByTaskIdentifier等属性。以及用于实现task和AFURLSessionManagerTaskDelegate的绑定的taskDescriptionForSessionTasks、还有关键操作的锁属性lock。

    • 接口文件的各种Block对应的属性,一个Block对应一个属性。

    • 处理Task暂停与重启操作的方法。

    • 给Task设置AFURLSessionManagerTaskDelegate代理的方法。

    • 初始化Task的各种方法。

    • 设置B接口文件定义的各种Block。

    • NSURLSession系列代理方法。

  • _AFURLSessionTaskSwizzling私有类。主要实现了iOS7和iOS8系统上NSURLSession差别的处理。让不同系统版本NSURLSession版本基本一致。

  • AFURLSessionManagerTaskDelegate这个类主要是把NSURLSeesion的部分代理方法让他处理。从而达到简化代码的目的。

    • 处理Task的上传或者下载进度。

    • 处理封装NSURLSeesion返回的数据。

    • Task完成等的通知封装。

  • 全局dispatch_queue_tdispatch_group_t的定义。各种通知名称的初始化,各种Block的类型定义。

3.1 AFURLSessionManager一个网络请求实现过程

我们通过一个网络请求过程来分析AFURLSessionManager.m的实现。我们通过initWithSessionConfiguration方法初始化一个manager。在这个方法里会初始化各种属性、以及为session属性设置代理:

接口文件中的代码如下:

 AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];

实现文件中对应的处理如下:

/**
 初始化方法
 @return 返回一个manager对象
 */
- (instancetype)init {
    return [self initWithSessionConfiguration:nil];
}
/**
 默认初始化方法、通过这个方法来做manager的具体化初始化动作

 @param configuration NSURLSession的配置
 @return 返回一个manager对象
 */
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
    self = [super init];
    if (!self) {
        return nil;
    }
    //如果用户没有手动指定,则使用默认的configuration来初始化
    if (!configuration) {
        configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
    }
    //赋值给属性
    self.sessionConfiguration = configuration;
    //初始化NSURLSession的task代理方法执行的队列。
    //这里有一个很关键的点是task的代理执行的queque一次性只能执行一个task。这样就避免了task的代理方法执行的混乱。
    self.operationQueue = [[NSOperationQueue alloc] init];
    self.operationQueue.maxConcurrentOperationCount = 1;
    //出丝滑NSURLSession对象,最核心的对象。
    self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
    //如果用户没有手动指定,则返回的数据是JSON格式序列化。
    self.responseSerializer = [AFJSONResponseSerializer serializer];
    //指定https处理的安全策略。
    self.securityPolicy = [AFSecurityPolicy defaultPolicy];
#if !TARGET_OS_WATCH
    //初始化网络状态监听属性
    self.reachabilityManager = [AFNetworkReachabilityManager sharedManager];
#endif
    //用于记录Task与他的`AFURLSessionManagerTaskDelegate`代理对象的一一对应关系。通过这个
    self.mutableTaskDelegatesKeyedByTaskIdentifier = [[NSMutableDictionary alloc] init];
    //初始化一个锁对象,关键操作加锁。
    self.lock = [[NSLock alloc] init];
    self.lock.name = AFURLSessionManagerLockName;
    /**
     获取当前session正在执行的所有Task。同时为每一个Task添加`AFURLSessionManagerTaskDelegate`代理对象,这个代理对象主要用于管理uplaodTak和downloadTask的进度管理。并且在Task执行完毕以后调用相应的Block。同时发送相应的notification对象,实现对task数据或者状态改变的检测。
     @param dataTasks dataTask列表
     @param uploadTasks uplaodTask列表
     @param downloadTasks downloadTask列表
     @return
     */
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        for (NSURLSessionDataTask *task in dataTasks) {
            [self addDelegateForDataTask:task uploadProgress:nil downloadProgress:nil completionHandler:nil];
        }
        for (NSURLSessionUploadTask *uploadTask in uploadTasks) {
            [self addDelegateForUploadTask:uploadTask progress:nil completionHandler:nil];
        }
        for (NSURLSessionDownloadTask *downloadTask in downloadTasks) {
            [self addDelegateForDownloadTask:downloadTask progress:nil destination:nil completionHandler:nil];
        }
    }];
    return self;
}

请求执行,接口文件如下:

NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request progress:^(NSProgress *downloadProgress){
    NSLog(@"下载进度:%lld",downloadProgress.completedUnitCount);
} destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
    NSURL *documentsDirectoryURL = [[NSFileManager defaultManager] URLForDirectory:NSDocumentDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
    NSURL *fileURL = [documentsDirectoryURL URLByAppendingPathComponent:[response suggestedFilename]];
    NSLog(@"fileURL:%@",[fileURL absoluteString]);
    return fileURL;
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
    self.imageView.image = [UIImage imageWithData:[NSData dataWithContentsOfURL:filePath]];
    NSLog(@"File downloaded to: %@", filePath);
}];

实现文件则调用了很多方法:

1 首先是初始化一个NSURLSessionDownLoadTask对象

    //通过session创建一个downloadTask,
    __block NSURLSessionDownloadTask *downloadTask = nil;
    //url_session_manager_create_task_safely作用是修复在iOS8下面的系统bug。
    url_session_manager_create_task_safely(^{
        downloadTask = [self.session downloadTaskWithRequest:request];
    });
    [self addDelegateForDownloadTask:downloadTask progress:downloadProgressBlock destination:destination completionHandler:completionHandler];
    return downloadTask;

2 通过[self addDelegateForDownloadTask:downloadTask progress:downloadProgressBlock destination:destination completionHandler:completionHandler];这句话来为Task设置一个AFURLSessionManagerTaskDelegate代理对象。从而可以实现对进度处理、Block调用、Task完成返回数据的拼装的功能。

    //根据指定的Task,初始化一个AFURLSessionManagerTaskDelegate
    AFURLSessionManagerTaskDelegate *delegate = [[AFURLSessionManagerTaskDelegate alloc] initWithTask:downloadTask];
    delegate.manager = self;
    //设置Task完成的回调Block
    delegate.completionHandler = completionHandler;
    if (destination) {
        //任务完成以后,调用destination这个Block
        delegate.downloadTaskDidFinishDownloading = ^NSURL * (NSURLSession * __unused session, NSURLSessionDownloadTask *task, NSURL *location) {
            return destination(location, task.response);
        };
    }
    //指定Task与taskDescriptionForSessionTasks的关联关系,方便后面的通知中做对应的处理。
    downloadTask.taskDescription = self.taskDescriptionForSessionTasks;
    //添加通知
    [self setDelegate:delegate forTask:downloadTask];
    //设置一个下载进度的Block,以便在后面代理方法中调用。
    delegate.downloadProgressBlock = downloadProgressBlock;

3 初始化一个AFURLSessionManagerTaskDelegate对象。在这个对象中对Task的请求过程进行处理和控制。

/**
 初始化一个AFURLSessionManagerTaskDelegate对象
 @param task 对象绑定的Task
 @return 返回对象
 */
- (instancetype)initWithTask:(NSURLSessionTask *)task {
    self = [super init];
    if (!self) {
        return nil;
    }
    //这个属性用于存储Task下载过程中的数据
    _mutableData = [NSMutableData data];
    //存储Task上传和下载的进度
    _uploadProgress = [[NSProgress alloc] initWithParent:nil userInfo:nil];
    _downloadProgress = [[NSProgress alloc] initWithParent:nil userInfo:nil];
    __weak __typeof__(task) weakTask = task;
    for (NSProgress *progress in @[ _uploadProgress, _downloadProgress ])
    {
        progress.totalUnitCount = NSURLSessionTransferSizeUnknown;
        progress.cancellable = YES;
        //当progress对象取消的时候,取消Task
        progress.cancellationHandler = ^{
            [weakTask cancel];
        };
        progress.pausable = YES;
        progress.pausingHandler = ^{
            //挂起Task
            [weakTask suspend];
        };
        if ([progress respondsToSelector:@selector(setResumingHandler:)]) {
            progress.resumingHandler = ^{
                //重启Task
                [weakTask resume];
            };
        }
        //更具progress的进度来获取Task的进度。fractionCompleted方法在请求过程中多次执行。
        [progress addObserver:self
                   forKeyPath:NSStringFromSelector(@selector(fractionCompleted))
                      options:NSKeyValueObservingOptionNew
                      context:NULL];
    }
    return self;
}
//上面通过对fractionCompleted方法KVO。则会调用下面的方法,从而执行manager的
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
   if ([object isEqual:self.downloadProgress]) {
       //更新下载进度Block
        if (self.downloadProgressBlock) {
            self.downloadProgressBlock(object);
        }
    }else if ([object isEqual:self.uploadProgress]) {
        //更新上传进度Bloc
        if (self.uploadProgressBlock) {
            self.uploadProgressBlock(object);
        }
    }
}

4 在AFURLSessionManagerTaskDelegate设置Task状态改变的监听。

/**
 设置指定task的`AFURLSessionManagerTaskDelegate`对象。并且添加task挂起或者重启的监听。
 @param delegate 代理对象
 @param task task
 */
- (void)setDelegate:(AFURLSessionManagerTaskDelegate *)delegate
            forTask:(NSURLSessionTask *)task
{
    NSParameterAssert(task);
    NSParameterAssert(delegate);
    //加锁操作
    [self.lock lock];
    //为Task设置与之代理方法关联关系。通过一个字典
    self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
    //添加对Task开始、重启、挂起状态的通知的接收。
    [self addNotificationObserverForTask:task];
    [self.lock unlock];
}
/**
 给Task添加任务开始、重启、挂起的通知

 @param task 任务
 */
- (void)addNotificationObserverForTask:(NSURLSessionTask *)task {
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskDidResume:) name:AFNSURLSessionTaskDidResumeNotification object:task];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(taskDidSuspend:) name:AFNSURLSessionTaskDidSuspendNotification object:task];
}

5 从下面开始,任务就正式开始执行。其实就是[downloadTask resume];执行以后开始。

/**
 在网络请求正式开始以后,这个方法会在数据接收的过程中多次调用。我们可以通过这个方法获取数据下载的大小、总得大小、还有多少么有下载
 @param session session
 @param downloadTask 对应的Task
 @param bytesWritten 已经下载的字节
 @param totalBytesWritten 总的字节大小
 @param totalBytesExpectedToWrite nil
 */
- (void)URLSession:(NSURLSession *)session
      downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
{
    //获取Task对应的`AFURLSessionManagerTaskDelegate`对象。从而可以调用对应的代理方法
    AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:downloadTask];
    if (delegate) {
        //调用`AFURLSessionManagerTaskDelegate`类中的代理方法。从而实现对于进度更新等功能。
        //会调用下面的那个方法
        [delegate URLSession:session downloadTask:downloadTask didWriteData:bytesWritten totalBytesWritten:totalBytesWritten totalBytesExpectedToWrite:totalBytesExpectedToWrite];
    }
    if (self.downloadTaskDidWriteData) {
        //如果有`downloadTaskDidWriteData`Block的实现,则在这个调用Block从而实现对下载进度过程的控制。
        self.downloadTaskDidWriteData(session, downloadTask, bytesWritten, totalBytesWritten, totalBytesExpectedToWrite);
    }
}
//AFURLSessionManagerTaskDelegate里面的这个代理方法实现对进度的更新。
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
    //AFURLSessionManagerTaskDelegate代理方法实现对下载进度的记录
    self.downloadProgress.totalUnitCount = totalBytesExpectedToWrite;
    self.downloadProgress.completedUnitCount = totalBytesWritten;
}

6 Task完成以后,会调用AFURLSessionManagerTaskDelegate对象的方法对返回的数据封装。

//AFURLSessionManagerTaskDelegate里面的这个代理方法实现对数据的具体处理。
- (void)URLSession:(__unused NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    //获取Task对应的manager对象
    __strong AFURLSessionManager *manager = self.manager;
    //要封装的responseObject对象。
    __block id responseObject = nil;
    __block NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
    userInfo[AFNetworkingTaskDidCompleteResponseSerializerKey] = manager.responseSerializer;
    //返回的数据。
    NSData *data = nil;
    if (self.mutableData) {
        data = [self.mutableData copy];
        //We no longer need the reference, so nil it out to gain back some memory.
        self.mutableData = nil;
    }
    //如果是downloadTask,则封装downloadFileURL
    if (self.downloadFileURL) {
        userInfo[AFNetworkingTaskDidCompleteAssetPathKey] = self.downloadFileURL;
    } else if (data) {//如果是其他Task,则封装返回的data。
        userInfo[AFNetworkingTaskDidCompleteResponseDataKey] = data;
    }
    //有错封装
    if (error) {
        userInfo[AFNetworkingTaskDidCompleteErrorKey] = error;
        dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
            //如果Task有completionHandler。则调用这个Block
            if (self.completionHandler) {
                self.completionHandler(task.response, responseObject, error);
            }
            //发送一个指定Task结束的通知
            dispatch_async(dispatch_get_main_queue(), ^{
                [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo];
            });
        });
    } else {//正确数据封装
        //在一个并行的dispat_queuq_t对象里面异步处理。
        dispatch_async(url_session_manager_processing_queue(), ^{
            NSError *serializationError = nil;
            //封装responseBojct
            responseObject = [manager.responseSerializer responseObjectForResponse:task.response data:data error:&serializationError];
            if (self.downloadFileURL) {
                responseObject = self.downloadFileURL;
            }
            if (responseObject) {
                userInfo[AFNetworkingTaskDidCompleteSerializedResponseKey] = responseObject;
            }
            if (serializationError) {
                userInfo[AFNetworkingTaskDidCompleteErrorKey] = serializationError;
            }
            dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
                //如果Task有完成Block。则调用这个Block
                if (self.completionHandler) {
                    self.completionHandler(task.response, responseObject, serializationError);
                }
                //发送通知
                dispatch_async(dispatch_get_main_queue(), ^{
                    [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo];
                });
            });
        });
    }
}

7 移除Task对应的通知和对应的AFURLSessionManagerTaskDelegate代理对象。

- (void)removeDelegateForTask:(NSURLSessionTask *)task {
    NSParameterAssert(task);
    [self.lock lock];
    //移除Task对应的通知
    [self removeNotificationObserverForTask:task];
    //移除Task对应的`AFURLSessionManagerTaskDelegate`代理对象。
    [self.mutableTaskDelegatesKeyedByTaskIdentifier removeObjectForKey:@(task.taskIdentifier)];
    [self.lock unlock];
}
//移除通知监听
- (void)removeNotificationObserverForTask:(NSURLSessionTask *)task {
    [[NSNotificationCenter defaultCenter] removeObserver:self name:AFNSURLSessionTaskDidSuspendNotification object:task];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:AFNSURLSessionTaskDidResumeNotification object:task];
}
//`AFURLSessionManagerTaskDelegate`对象回收。
- (void)dealloc {
    [self.downloadProgress removeObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted))];
    [self.uploadProgress removeObserver:self forKeyPath:NSStringFromSelector(@selector(fractionCompleted))];
}

通过上面的过程,我们发现核心流程都是围绕了NSRULSessionTask对象以及与之绑定的AFURLSessionManagerTaskDelegate对象执行的。我们通过在NSRULSessionTask对象的代理方法里面手动调用AFURLSessionManagerTaskDelegate对应的代理方法来实现对数据的处理和简化代码的作用,这个设计思路的确吊吊的。还有一些方法没有涉及到,不过大同小异,基本过程就是这样,就不一一解释了。

3.2 AFURLSessionManager一些特殊模块的说明

AFURLSeeesionManager实现了NSSecureCoding协议。让manager可以归档解档。

/**
 在iOS8以及以上环境下,supportsSecureCoding必须重写并且返回true。
 @return bool
 */
+ (BOOL)supportsSecureCoding {
    return YES;
}
//解档
- (instancetype)initWithCoder:(NSCoder *)decoder {
    NSURLSessionConfiguration *configuration = [decoder decodeObjectOfClass:[NSURLSessionConfiguration class] forKey:@"sessionConfiguration"];
    self = [self initWithSessionConfiguration:configuration];
    if (!self) {
        return nil;
    }
    return self;
}
/**
 我们发现对象归档的时候,只归档了`NSURLSessionConfiguration`属性。所以说归档接档的时候所有Block设置、operation设置都会失效。
 @param coder coder
 */
- (void)encodeWithCoder:(NSCoder *)coder {
    [coder encodeObject:self.session.configuration forKey:@"sessionConfiguration"];
}

同时,AFURLSessionManager也实现了NSCopying协议。通过协议的实现过程,我们发现也是只使用了NSURLSessionConfiguration属性。和归档解档一样。

#pragma mark - 实现NSCopying协议。copy的NAURLSessionManager没有复制任何与代理处理相关的Block
- (instancetype)copyWithZone:(NSZone *)zone {
    return [[[self class] allocWithZone:zone] initWithSessionConfiguration:self.session.configuration];
}

有的时候,我们的请求会返回302这个状态码,这个表示需要请求重定向到另一个url,我们可以下面这个代理方法里面决定对于重定向的处理,如果对completionHandler传入nil,则会把response传入重定向请求。另外,backgroundSession的Task不会调用下面这个代理方法,而是直接调用。

/**
 有的时候,我们的请求会返回302这个状态码,这个表示需要请求重定向到另一个url,我们可以在这个代理方法里面绝定对于重定向的处理。
 @param session session
 @param task task
 @param response response
 @param request 重定向的request。
 @param completionHandler 请求完成
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest *))completionHandler
{
    //重定向的request对象
    NSURLRequest *redirectRequest = request;
    //如果用户指定了taskWillPerformHTTPRedirection这个Block,我们就通过这个Block的调用返回处理完成的request对象。
    if (self.taskWillPerformHTTPRedirection) {
        redirectRequest = self.taskWillPerformHTTPRedirection(session, task, response, request);
    }
    //这个调用是必须的,执行重定向操作。
    if (completionHandler) {
        completionHandler(redirectRequest);
    }
}

创建NSRULSessionUplaodTask的时候,在某些系统上会出现bug。AFN已经帮我们处理好:

- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL progress:(void (^)(NSProgress *uploadProgress)) uploadProgressBlock completionHandler:(void (^)(NSURLResponse *response, id responseObject, NSError *error))completionHandler
{
    __block NSURLSessionUploadTask *uploadTask = nil;
    //用线程安全的方式创建一个dataTask。修复iOS8下面的bug。
    url_session_manager_create_task_safely(^{
        uploadTask = [self.session uploadTaskWithRequest:request fromFile:fileURL];
    });
    //用于处理uploadTask在iOS7环境下面有可能创建失败的情况。如果attemptsToRecreateUploadTasksForBackgroundSessions为true。则尝试重新创建Task。如果三次都没有成功,则放弃。
    if (!uploadTask && self.attemptsToRecreateUploadTasksForBackgroundSessions && self.session.configuration.identifier) {
        for (NSUInteger attempts = 0; !uploadTask && attempts < AFMaximumNumberOfAttemptsToRecreateBackgroundSessionUploadTask; attempts++) {
            uploadTask = [self.session uploadTaskWithRequest:request fromFile:fileURL];
        }
    }
    //为Task添加`AFURLSessionManagerTaskDelegate`代理方法
    [self addDelegateForUploadTask:uploadTask progress:uploadProgressBlock completionHandler:completionHandler];
    return uploadTask;
}

通过使用dispatch_semaphore_t来控制对异步处理返回结果的控制。非常有借鉴意义。

#pragma mark -  获取当前session对应的task列表。通过dispatch_semaphore_t来控制访问过程。
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }
        //这里发送一个信号量,让semaphore变为1。此时表示tasks已经成功获取。
        dispatch_semaphore_signal(semaphore);
    }];
    //这里会一直等待信号量变为1。
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    //返回Task。通过信号量控制,避免了方法结束的时候,tasks还没有正常获取的情况。
    return tasks;
}

4 _AFURLSessionTaskSwizzling私有类的说明

在iOS7和iOS8及以上的系统,NSRULSessionTask的具体实现是不同的。我们目前知道的不同有:

  • NSURLSessionTasks是一个类簇。所以我们初始化一个Task的时候,我们并不只到初始化的到底是哪个子类。

  • 简单的通过[NSURLSessionTask class]并不会起作用。必须通过NSURLSession创建一个task对象。然后获取他所在的类。

  • iOS7下面,下面代码中的localDataTask对象的继承关系是__NSCFLocalDataTask -> __NSCFLocalSessionTask -> __NSCFURLSessionTask

  • 在iOS8以及以上系统。下面代码中的localDataTask对象的继承关系是__NSCFLocalDataTask -> __NSCFLocalSessionTask -> NSURLSessionTask

  • 在iOS7下面__NSCFLocalSessionTask__NSCFURLSessionTask实现了resumesuspend方法,同时最重要的是他不调用父类的实现。但是iOS8下面,只有NSURLSessionTask实现了resumesuspend。所以在iOS7的环境下,我们需要想办法让resumesuspend调用NSURLSessionTask的具体实现。

下面的代码完美的向我们展示了一个向类添加方法,并且swizzle方法实现的过程。值得仔细琢磨。

/**
 切换theClass类的`originalSelector`和`swizzledSelector`的实现
 @param theClass 类
 @param originalSelector 方法一
 @param swizzledSelector 方法2
 */
static inline void af_swizzleSelector(Class theClass, SEL originalSelector, SEL swizzledSelector) {
    Method originalMethod = class_getInstanceMethod(theClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSelector);
    method_exchangeImplementations(originalMethod, swizzledMethod);
}
/**
 动态给一个类添加方法
 @param theClass 类
 @param selector 方法名字
 @param method 方法体
 @return bool
 */
static inline BOOL af_addMethod(Class theClass, SEL selector, Method method) {
    return class_addMethod(theClass, selector,  method_getImplementation(method),  method_getTypeEncoding(method));
}
@implementation _AFURLSessionTaskSwizzling
+ (void)load {
    if (NSClassFromString(@"NSURLSessionTask")) {
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
        NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnonnull"
        //初始化一个dataTask对象
        NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil];
#pragma clang diagnostic pop
        //获取af_resume这个方法的实现。
        IMP originalAFResumeIMP = method_getImplementation(class_getInstanceMethod([self class], @selector(af_resume)));
        //获取dataTask的具体类
        Class currentClass = [localDataTask class];
        //如果父类有resume方法。则改变方法的具体实现。
        while (class_getInstanceMethod(currentClass, @selector(resume))) {
            Class superClass = [currentClass superclass];
            //找到类和父类的resume方法实现
            IMP classResumeIMP = method_getImplementation(class_getInstanceMethod(currentClass, @selector(resume)));
            IMP superclassResumeIMP = method_getImplementation(class_getInstanceMethod(superClass, @selector(resume)));
            if (classResumeIMP != superclassResumeIMP &&
                originalAFResumeIMP != classResumeIMP) {
                //添加方法、然后转换方法的实现
                [self swizzleResumeAndSuspendMethodForClass:currentClass];
            }
            currentClass = [currentClass superclass];
        }
        [localDataTask cancel];
        [session finishTasksAndInvalidate];
    }
}
/**
 主要是实现了为一个类添加方法、并且转换添加方法和原来对应方法的实现。
 @param theClass 要操作的类
 */
+ (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass {
    Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume));
    Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend));
    //为theClass类添加一个af_resume方法。
    if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) {
        //把dataTask的resume和afresume方法的实现互换。
        af_swizzleSelector(theClass, @selector(resume), @selector(af_resume));
    }
    //为theClass类添加一个af_suspend方法
    if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) {
        //把dataTask的suspend和af_suspend方法的实现互换。
        af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend));
    }
}
- (NSURLSessionTaskState)state {
    NSAssert(NO, @"State method should never be called in the actual dummy class");
    return NSURLSessionTaskStateCanceling;
}
/**
 在iOS7下面,`NSURLSessionDataTask`调用resume方法其实就是执行`af_resume`的具体实现。
 */
- (void)af_resume {
    NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
    NSURLSessionTaskState state = [self state];
    //这里其实就是调用dataTask的resume实现
    [self af_resume];
    if (state != NSURLSessionTaskStateRunning) {
        //这里的self其实就是`NSRULSessionDataTask`对象
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidResumeNotification object:self];
    }
}
/**
 在iOS7下面,`NSURLSessionDataTask`调用suspend方法其实就是执行`af_suspend`的具体实现。
 */
- (void)af_suspend {
    NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
    NSURLSessionTaskState state = [self state];
    //这里其实就是调用dataTask的suspend具体实现
    [self af_suspend];
    if (state != NSURLSessionTaskStateSuspended) {
        //这里的self其实就是`NSRULSessionDataTask`对象
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidSuspendNotification object:self];
    }
}
@end

5 总结

AFURLSessionManager通过对task设置一个AFURLSessionManagerTaskDelegate代理来处理繁杂的请求进度管理。从而降低了代码的负责度。是代理模式的一个很好的实践。

AFURLSessionManager通过私有类_AFURLSessionTaskSwizzling来修改iOS7和iOS8系统上面不同。是对于方法swizzle的一个成功和完整的实践。

AFURLSessionManager通过添加各种Block,让我们对请求过程有全方位的控制和处理。同时提供简洁的api,把负责的处理全部封装好。

源码地址iOSSourceCodeStudy,博客地址博客地址

查看原文

赞 0 收藏 0 评论 0

huang303513 发布了文章 · 2017-04-15

UIViewController和UIView不同加载方式的生命周期函数

1 基本说明

话说做了iOS几年,对于UIViewController和UIView的生命周期函数一直不是很重视,导致了很多模糊的地方。今天专门写一个Demo来验证一下,发现不同加载方式差别还是蛮大的。

2 加载UIViewController

每一种加载方式所调用的加载方法不同,而且还有一些细节地方不同。苹果为我们提供了四种默认的加载方式,不过我是纯代码党,AutoLayout以后,我们通过Masonry这个第三方布局利器来布局,在我Git上也有专门总结的不同布局解决方案。接下来我们看看系统的四种方式,犹豫没有什么理论性的东西,我就直接上代码为主了:

  • 通过XIB加载。

  • 通过StoryBoard加载。

  • 通过NSCoding协议加载。

  • 通过代码加载。

2.1 用XIB加载UIViewController

首先看初始化代码:

- (IBAction)loadControllerFromXIB:(id)sender {
    XIBViewController *xibVC = [[XIBViewController alloc]initWithNibName:@"XIBViewController" bundle:[NSBundle mainBundle]];
    [self.navigationController pushViewController:xibVC animated:YES];
}

当我们不实现loadView的时候打印结果:

2017-04-15 12:05:32.974 UIViewController和UIView生命周期加载和卸载[59883:1192231] initWithNibName
2017-04-15 12:05:32.987 UIViewController和UIView生命周期加载和卸载[59883:1192231] viewDidLoad
2017-04-15 12:05:32.987 UIViewController和UIView生命周期加载和卸载[59883:1192231] viewWillAppear
2017-04-15 12:05:32.996 UIViewController和UIView生命周期加载和卸载[59883:1192231] viewWillLayoutSubviews
2017-04-15 12:05:32.997 UIViewController和UIView生命周期加载和卸载[59883:1192231] viewDidLayoutSubviews
2017-04-15 12:05:33.002 UIViewController和UIView生命周期加载和卸载[59883:1192231] viewWillLayoutSubviews
2017-04-15 12:05:33.002 UIViewController和UIView生命周期加载和卸载[59883:1192231] viewDidLayoutSubviews
2017-04-15 12:05:33.506 UIViewController和UIView生命周期加载和卸载[59883:1192231] viewDidAppear
2017-04-15 12:05:37.142 UIViewController和UIView生命周期加载和卸载[59883:1192231] clickButton
//pop以后
2017-04-15 12:05:42.334 UIViewController和UIView生命周期加载和卸载[59883:1192231] viewWillDisappear
2017-04-15 12:05:42.837 UIViewController和UIView生命周期加载和卸载[59883:1192231] viewDidDisappear
2017-04-15 12:05:42.838 UIViewController和UIView生命周期加载和卸载[59883:1192231] dealloc

我发现一个很奇怪的现象。如果用XIB加载的控制器,并且实现了一个空loadView,那么我们在XIB设置的视图都失效了,应该是系统返回了一个默认的视图覆盖了。但是用Storyboard加载的视图,实现一个空的loadView则不会丢失Storyboard里面的视图,这个是用XIB和Storyboard的一个注意点

-(void)loadView{
    [super loadView];
    NSLog(@"loadView");
}

2.2 用Storyboard加载UIViewController

初始化代码:

- (IBAction)laodControllerFromSB:(id)sender {
    UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Second" bundle:[NSBundle mainBundle]];
    SBViewController *sbVC = [sb instantiateViewControllerWithIdentifier:@"SBViewController"];
    [self.navigationController pushViewController:sbVC animated:YES];
}

运行结果:

2017-04-15 12:26:45.364 UIViewController和UIView生命周期加载和卸载[59932:1194239] initWithCoder
2017-04-15 12:26:45.365 UIViewController和UIView生命周期加载和卸载[59932:1194239] awakeFromNib
2017-04-15 12:26:45.368 UIViewController和UIView生命周期加载和卸载[59932:1194239] loadView
2017-04-15 12:26:45.368 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewDidLoad
2017-04-15 12:26:45.368 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewWillAppear
2017-04-15 12:26:45.372 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewWillLayoutSubviews
2017-04-15 12:26:45.373 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewDidLayoutSubviews
2017-04-15 12:26:45.877 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewDidAppear
//pop以后
2017-04-15 12:26:50.669 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewWillDisappear
2017-04-15 12:26:51.172 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewDidDisappear
2017-04-15 12:26:51.172 UIViewController和UIView生命周期加载和卸载[59932:1194239] dealloc

对于Storyboard,使用一个空的loadView没有影响。

2.3 用NSCodeing加载UIViewController

初始化代码:

//这里我就不实现NSCoding协议了,直接传入一个nil参数。
- (IBAction)loadControllerFromCoder:(id)sender {
    CoderViewController *coderVC = [[CoderViewController alloc]initWithCoder:nil];
    [self.navigationController pushViewController:coderVC animated:YES];
}

运行结果:

2017-04-15 12:30:25.962 UIViewController和UIView生命周期加载和卸载[59932:1194239] initWithCoder
2017-04-15 12:30:25.963 UIViewController和UIView生命周期加载和卸载[59932:1194239] loadView
2017-04-15 12:30:25.963 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewDidLoad
2017-04-15 12:30:25.963 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewWillAppear
2017-04-15 12:30:25.967 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewWillLayoutSubviews
2017-04-15 12:30:25.967 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewDidLayoutSubviews
2017-04-15 12:30:25.968 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewWillLayoutSubviews
2017-04-15 12:30:25.968 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewDidLayoutSubviews
2017-04-15 12:30:26.470 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewDidAppear
//pop以后
2017-04-15 12:30:28.034 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewWillDisappear
2017-04-15 12:30:28.537 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewDidDisappear
2017-04-15 12:30:28.537 UIViewController和UIView生命周期加载和卸载[59932:1194239] dealloc

2.4 用代码加载UIViewController

初始化代码:

- (IBAction)loadControllerWithNone:(id)sender {
    CodeViewController *codeVC = [[CodeViewController alloc]init];
    [self.navigationController pushViewController:codeVC animated:YES];
}

运行结果:

2017-04-15 12:31:48.785 UIViewController和UIView生命周期加载和卸载[59932:1194239] initWithNibName
2017-04-15 12:31:48.786 UIViewController和UIView生命周期加载和卸载[59932:1194239] init
2017-04-15 12:31:48.787 UIViewController和UIView生命周期加载和卸载[59932:1194239] loadView
2017-04-15 12:31:48.787 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewDidLoad
2017-04-15 12:31:48.788 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewWillAppear
2017-04-15 12:31:48.792 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewWillLayoutSubviews
2017-04-15 12:31:48.792 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewDidLayoutSubviews
2017-04-15 12:31:48.792 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewWillLayoutSubviews
2017-04-15 12:31:48.792 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewDidLayoutSubviews
2017-04-15 12:31:49.293 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewDidAppear
//pop以后
2017-04-15 12:31:55.594 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewWillDisappear
2017-04-15 12:31:56.098 UIViewController和UIView生命周期加载和卸载[59932:1194239] viewDidDisappear
2017-04-15 12:31:56.098 UIViewController和UIView生命周期加载和卸载[59932:1194239] dealloc

3 加载UIView

3.1 用XIB加载UIView

初始化代码:

- (IBAction)loadViewFromXib:(id)sender {
    XibView *xibView = [[[NSBundle mainBundle]loadNibNamed:@"XIBView" owner:self options:nil] lastObject];
    [self.view addSubview:xibView];
}

运行结果:

2017-04-15 12:33:22.194 UIViewController和UIView生命周期加载和卸载[59932:1194239] initWithCoder
2017-04-15 12:33:22.195 UIViewController和UIView生命周期加载和卸载[59932:1194239] awakeFromNib
2017-04-15 12:33:22.195 UIViewController和UIView生命周期加载和卸载[59932:1194239] willMoveToWindow
2017-04-15 12:33:22.195 UIViewController和UIView生命周期加载和卸载[59932:1194239] willMoveToSuperview
2017-04-15 12:33:22.196 UIViewController和UIView生命周期加载和卸载[59932:1194239] didMoveToWindow
2017-04-15 12:33:22.196 UIViewController和UIView生命周期加载和卸载[59932:1194239] didMoveToSuperview
2017-04-15 12:33:22.197 UIViewController和UIView生命周期加载和卸载[59932:1194239] layoutSubviews
//这里是点击移除以后
2017-04-15 12:33:25.769 UIViewController和UIView生命周期加载和卸载[59932:1194239] willMoveToSuperview
2017-04-15 12:33:25.770 UIViewController和UIView生命周期加载和卸载[59932:1194239] willMoveToWindow
2017-04-15 12:33:25.771 UIViewController和UIView生命周期加载和卸载[59932:1194239] didMoveToWindow
2017-04-15 12:33:25.771 UIViewController和UIView生命周期加载和卸载[59932:1194239] didMoveToSuperview
2017-04-15 12:33:25.771 UIViewController和UIView生命周期加载和卸载[59932:1194239] dealloc

3.2 用代码加载UIView

初始化代码:

- (IBAction)loadViewWithNone:(id)sender {
    CodeView *codeView = [[CodeView alloc]init];
    codeView.backgroundColor = [UIColor greenColor];
    codeView.frame = CGRectMake(0, 500, 100, 50);
    [self.view addSubview:codeView];
}

运行结果:

2017-04-15 12:38:57.562 UIViewController和UIView生命周期加载和卸载[60323:1208772] initWithFrame
2017-04-15 12:38:57.562 UIViewController和UIView生命周期加载和卸载[60323:1208772] init
2017-04-15 12:38:57.562 UIViewController和UIView生命周期加载和卸载[60323:1208772] willMoveToWindow
2017-04-15 12:38:57.563 UIViewController和UIView生命周期加载和卸载[60323:1208772] willMoveToSuperview
2017-04-15 12:38:57.563 UIViewController和UIView生命周期加载和卸载[60323:1208772] didMoveToWindow
2017-04-15 12:38:57.563 UIViewController和UIView生命周期加载和卸载[60323:1208772] didMoveToSuperview
2017-04-15 12:38:57.564 UIViewController和UIView生命周期加载和卸载[60323:1208772] layoutSubviews
//点击移除以后
2017-04-15 12:39:02.751 UIViewController和UIView生命周期加载和卸载[60323:1208772] willMoveToSuperview
2017-04-15 12:39:02.752 UIViewController和UIView生命周期加载和卸载[60323:1208772] willMoveToWindow
2017-04-15 12:39:02.752 UIViewController和UIView生命周期加载和卸载[60323:1208772] didMoveToWindow
2017-04-15 12:39:02.752 UIViewController和UIView生命周期加载和卸载[60323:1208772] didMoveToSuperview
2017-04-15 12:39:02.752 UIViewController和UIView生命周期加载和卸载[60323:1208772] 点击移除
2017-04-15 12:39:02.753 UIViewController和UIView生命周期加载和卸载[60323:1208772] dealloc

4 总结

UIViewController不同加载方式钩子函数总结:

  • XIB加载方式

    • initWithNibName

    • loadView(注意:如果实现一个空的方法,则XIB设置的属性无效,会覆盖XIB中的设置)

    • viewDidLoad

  • Storyboard加载方式

    • initWithCoder

    • awakeFromNib

    • loadView(实现是一个空方法或者不实现没有影响)

    • viewDidLoad

  • NSCoding加载方式

    • initWithCoder

    • loadView

    • viewDidLoad

  • 代码加载方式

    • initWithNibName

    • init(这个是我初始化的时候主动调用,如果用initWithNibName传入nil参数则不会调用)

    • loadView

    • viewDidLoad

我们可以发现,代码加载方式和XIB加载方式一模一样,如果有XIB则加载XIB,如果没有XIB则可以代码添加视图。

UIView不同加载方式钩子函数总结:

  • XIB加载方式

    • initWithCoder

    • awakeFromNib

    • willMoveToWindow

  • 代码加载方式

    • initWithFrame(设置frame。)

    • init(init方法调用)

    • willMoveToWindow

我们发现,如果通过init初始化,然后手动设置Frame。则会导致上面的调用顺序。

源码链接UIViewController和UIView生命周期加载和卸载。博客地址博客地址

查看原文

赞 1 收藏 1 评论 0

huang303513 发布了文章 · 2017-04-14

AFNetWorking源码之NSURLSession系列概述

1 基本说明

记得我刚做iOS的时候,那时候还是ASI和AFN共存,甚至ASI使用比例还多点,一转眼几年过去,ASI基本已经消失了,AFN基本成了iOS项目的标配。我虽然以前也有看过AFN2.x的源码,但是对于AFN3.x的源码一直没有自己阅读。接下来我会对AFN3.x学习并且写博客记录。得益于NSURLSession的强大功能,ANF3.0放弃了NSURLConnection这一部分,让代码简化了很多,但是功能却更加丰富。我觉得在学习AFN之前,有必要仔细了解NSURLSessionhttps相关,不然会有很多地方迷惑不解,具体可以看我的git仓库![iOSSourceCodeStudy
](https://github.com/huang30351...。同时我强烈推荐浏览一下NSURLSession.h这个文件。

2 相互关系

我们首先来看一下一个简单的NSURLSession请求代码:

    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
    
    NSURLRequest *request = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:bigPic]];
    
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
    [dataTask resume];

从上面我们发现,我们要发送一个网络请求,需要新建一个NSURLSession,新建一个NSURLSession又需要一个NSURLSessionConfiguration,并且还需要一些代理方法。同时我们需要一个NSURLSessionDataTask。所以说,我们的NSRULSession
网络请求系统包括一个session、一个configuration、一个Task已经Task附带的delegate。

  • 一个NSURLSession,总共只有一个类,也是最核心的类,他有一个对应的代理NSURLSessionDelegate

  • 一个NSURLSessionConfiguration,总共有三种模式。

  • 一个NSURLSessionTaskNSURLSessionTask是抽闲类,对应的代理NSURLSessionTaskDelegate。我们具体使用的时候,会使用他的三种子类,而且每个子类都有对应的delegate。

3 一个NSURLSession

首先我们看一下NSRULSession.h里面关于NSURLSession的部分。我们把它分为初始化部分、属性部分、dataTask部分、uploadTask部分、downloadTask部分。也就是说其他很多类都是围绕着下面这几个api衍生的。后面我们会每个部分分析。

//初始化部分
@property (class, readonly, strong) NSURLSession *sharedSession;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(nullable id <NSURLSessionDelegate>)delegate delegateQueue:(nullable NSOperationQueue *)queue;

//属性部分
@property (readonly, retain) NSOperationQueue *delegateQueue;
@property (nullable, readonly, retain) id <NSURLSessionDelegate> delegate;
@property (readonly, copy) NSURLSessionConfiguration *configuration;

//dataTask部分
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request;
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url;

//uploadTask部分
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL;
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(NSData *)bodyData;
- (NSURLSessionUploadTask *)uploadTaskWithStreamedRequest:(NSURLRequest *)request;

//downloadTask部分
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request;
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url;
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData;

3.1 Block的NSURLSession的api

我们都知道,NSRULConnection除了一套使用代理的API,还有一套对应的使用Block的api。NSURLSession也不列外。使用这一套api就不用实现代理方法。和delegate一样,Block也有dataTask系列、downloadTask系列、uploadTask系列。具体看下面:

//dataTask系列
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler{
    
}
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler{
    
}
//unloadTast系列
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromFile:(NSURL *)fileURL completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler{
    
}
- (NSURLSessionUploadTask *)uploadTaskWithRequest:(NSURLRequest *)request fromData:(nullable NSData *)bodyData completionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler{
    
}
//downloadTask系列
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler{
    
}
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url completionHandler:(void (^)(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler{
    
}
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)resumeData completionHandler:(void (^)(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error))completionHandler{
    
}

3.2 Block的NSURLSession使用

用dataTask下载一张图片,然后用imageView显示。

-(IBAction)requestBlockTaskTest:(id)sender{
    [self clear];
    NSURLSession *session = [NSURLSession sharedSession];
    NSURLRequest *request = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:bigPic]];
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        UIImage *image = [[UIImage alloc]initWithData:data];
        self.imageView.image = image;
    }];
    [dataTask resume];
}

4 一个NSURLSessionConfiguration

首先看一下NSURLSessionConfiguration部分。从这个名字,我们可以预感到这个是与session的配置相关的,的确也是这样。总共有三种类型的configuratin,另外还有很多属性,比如配置缓存策略的requestCachePolicy,请求超时的timeoutIntervalForRequest,添加额外请求头的HTTPAdditionalHeaders,其他还有很多属性这里就不一一说了,具体看源码:

//默认的配置会将缓存存储在磁盘上
@property (class, readonly, strong) NSURLSessionConfiguration *defaultSessionConfiguration;
//第二种瞬时会话模式不会创建持久性存储的缓存.
@property (class, readonly, strong) NSURLSessionConfiguration *ephemeralSessionConfiguration;
//第三种后台会话模式允许程序在后台进行上传下载工作
+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier;
//各种属性
@property NSURLRequestCachePolicy requestCachePolicy;
@property NSTimeInterval timeoutIntervalForRequest;
@property (nullable, copy) NSDictionary *HTTPAdditionalHeaders;

5 一个NSURLSessionTask

从上面NSURLSession初始化一个请求的时候,我们发现NSURLSessionTask并不能直接使用,只能使用他的子类。具体如下:

  • NSURLSessionTask抽象类。有对应的代理NSURLSessionTaskDelegate,而且这个代理继承了NSURLSessionDelegate代理。

  • NSURLSessionDataTaskNSURLSessionTask的子类。有对应的代理NSURLSessionTaskDelegate,而且这个代理继承了NSURLSessionTaskDelegate代理。我们一般网络请求,就用这个类。

  • NSURLSessionDownloadTaskNSURLSessionTask的子类。有对应的代理NSURLSessionDownloadDelegate,而且这个代理继承了NSURLSessionTaskDelegate代理。这个主要用于下载大文件等。

  • NSURLSessionUploadTaskNSURLSessionDataTask的子类。有对应的代理及时父类代理NSURLSessionDownloadDelegate。这个主要用于处理上传请求如上传图片。

从上面我们发现Task和delegate有一套对应的继承关系:

  • NSURLSessionTask (抽象类,NSURLSessionTaskDelegate)

    • NSURLSessionDataTask (NSURLSessionDataDelegate)

      • NSURLSessionUploadTask (NSURLSessionDataDelegate)

    • NSURLSessionDownloadTask (NSURLSessionDownloadDelegate)

  • NSURLSessionDelegate

    • NSURLSessionTaskDelegate

      • NSURLSessionDataDelegate

      • NSURLSessionDownloadDelegate

从继承关系上,我们就可以理解在初始化的时候,只通过设置NSURLSession对象的delegate就可以了。因为根据不同的task,其实就是设置了不同的delegate。这个设计避免了多次设置delegate的情况,同时也根据不同的task实现不同的delegate方法。真是一个很绝妙的设计。

6 代理说明

6.1 NSURLSessionDelegate

接下来我们看看NSURLSession的delegate对象NSURLSessionDelegate的方法,当一个session遇到错误、或者需要认证、应用进入后台都会调用下面的代理方法:

//当一个session遇到系统错误或者未检测到的错误的时候,就会调用这个方法。
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error{

}
//当请求需要认证、或者https证书认证的时候,我们就需要在这个方法里面处理。
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler{
    
}
//如果应用进入后台、这个方法会被调用。我们在这里可以对session发起的请求做各种操作比如请求完成的回调等。
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {

}

6.2 NSURLSessionTaskDelegate

/*
 当请求重定向的时候调用这个方法。我们必须设置一个新的`NSURLRequest`对象传入completionHandler来重定向新的请求,但是当`session`是background模式的时候,这个方法不会被调用。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
        newRequest:(NSURLRequest *)request
 completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler{

}
/*
 当请求需要认证的时候调用这个方法。如果没有实现这个代理,那么请求认证这个过程不会被调用。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler{

}
/*
    如果请求需要一个新的请求体时,这个方法就会被调用。比如认证失败的时候,我们可以通过这个机会从新认证。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
 needNewBodyStream:(void (^)(NSInputStream * _Nullable bodyStream))completionHandler{

}
/*
 当我们上传数据的时候,我们可以通过这个代理方法获取上传进度。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
   didSendBodyData:(int64_t)bytesSent
    totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend{

}
/*
 当task的统计信息收集好了以后,调用这个方法。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {

}
/*
 当一个task出错的时候,会调用这个方法。如果error是nil,也会调用这个方法,表示task完成。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error{
    NSLog(@"数据返回以后,不管有错没错都回调用,如果没错,error及时nil");
}

6.3 NSURLSessionDataDelegate

/*
 当一个task接收到返回信息。当所有信息都接收完毕以后,completionHandler会被调用。我们可以在这里取消一个网络请求或者把一个datatask转换为downloadtask。如果没有实现这个代理方法,我们也可以通过task的response属性获取到对应的数据。background模式的uploadtask不会调用这个方法。
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler{

}
/*
 当一个datatask转换为一个downloadtask以后会调用。
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask{
    // 允许处理服务器的响应,才会继续接收服务器返回的数据
    completionHandler(NSURLSessionResponseAllow);
}
/*
 暂时忽略,这个是和数据流相关的。不管了
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didBecomeStreamTask:(NSURLSessionStreamTask *)streamTask{

}
/*
 当data可以使用的时候,调用这个方法。我们可以在这里获取data。
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data{

}
/*
 允许我们在这里调用completionHandler缓存data,或者传入nil来禁止缓存
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse * _Nullable cachedResponse))completionHandler{

}

6.4 NSURLSessionDownloadDelegate

/*
    当一个下载task任务完成以后,这个方法会被调用。我们可以在这里移动或者复制download的数据
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location{

}
/*
 获取下载进度
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{

}
/*
 重启一个下载任务(比如下载一半后停止然后过一点时间继续)。如果下载出错,`NSURLSessionDownloadTaskResumeData`里面包含重新开始下载的数据。
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
 didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes{

}

7 NSURLSession的综合使用案列

分别用三种不同方式下载一张图片然后在imageView上显示。

#import "ViewController.h"

static NSString *const bigPic = @"http://i1.piimg.com/4851/d1498fea89ae3bc1.png";
static NSString *const smallPic = @"http://i1.piimg.com/4851/97aef4680d359905.png";

@interface ViewController ()<NSURLSessionDelegate>
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property(nonatomic,strong)NSMutableData *data;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // Do any additional setup after loading the view, typically from a nib.
}

- (IBAction)requestDataTest:(id)sender {
     [self clear];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
    NSURLRequest *request = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:bigPic]];
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
    [dataTask resume];
}


- (IBAction)requestDownloadTest:(id)sender {
     [self clear];
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
    NSURLRequest *request = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:bigPic]];
    NSURLSessionDownloadTask *dataTask = [session downloadTaskWithRequest:request];
    [dataTask resume];
}

-(IBAction)requestBlockTaskTest:(id)sender{
    [self clear];
    NSURLSession *session = [NSURLSession sharedSession];
    NSURLRequest *request = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:bigPic]];
    NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        UIImage *image = [[UIImage alloc]initWithData:data];
        self.imageView.image = image;
    }];
    [dataTask resume];
}

-(void)clear{
    self.imageView.image = nil;
}

//==============================NSURLSessionDelegate========================
#pragma NSURLSessionDelegate
//当一个session遇到系统错误或者未检测到的错误的时候,就会调用这个方法。
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(nullable NSError *)error{

}
//当请求需要认证、或者https证书认证的时候,我们就需要在这个方法里面处理。
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler{
    
}
//如果应用进入后台、这个方法会被调用。我们在这里可以对session发起的请求做各种操作比如请求完成的回调等。
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {

}

//==================================NSURLSessionTaskDelegate====================
#pragma NSURLSessionTaskDelegate
/*
 当请求重定向的时候调用这个方法。我们必须设置一个新的`NSURLRequest`对象传入completionHandler来重定向新的请求,但是当`session`是background模式的时候,这个方法不会被调用。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
        newRequest:(NSURLRequest *)request
 completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler{

}
/*
 当请求需要认证的时候调用这个方法。如果没有实现这个代理,那么请求认证这个过程不会被调用。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler{

}
/*
    如果请求需要一个新的请求体时,这个方法就会被调用。比如认证失败的时候,我们可以通过这个机会从新认证。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
 needNewBodyStream:(void (^)(NSInputStream * _Nullable bodyStream))completionHandler{

}
/*
 当我们上传数据的时候,我们可以通过这个代理方法获取上传进度。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
   didSendBodyData:(int64_t)bytesSent
    totalBytesSent:(int64_t)totalBytesSent
totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend{
    NSLog(@"");
}
/*
 当task的统计信息收集好了以后,调用这个方法。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {

}
/*
 当一个task出错的时候,会调用这个方法。如果error是nil,也会调用这个方法,表示task完成。
 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error{
    NSLog(@"数据返回以后,不管有错没错都回调用,如果没错,error及时nil");
    if (self.data) {
        self.imageView.image = [UIImage imageWithData:self.data];
        self.data = nil;
    }
}

//==================================NSURLSessionDataDelegate=====================================
#pragma NSURLSessionDataDelegate
/*
 当一个task接收到返回信息。当所有信息都接收完毕以后,completionHandler会被调用。我们可以在这里取消一个网络请求或者把一个datatask转换为downloadtask。如果没有实现这个代理方法,我们也可以通过task的response属性获取到对应的数据。background模式的uploadtask不会调用这个方法。
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler{
    self.data = nil;
    self.data = [NSMutableData data];
    // 允许处理服务器的响应,才会继续接收服务器返回的数据
    completionHandler(NSURLSessionResponseAllow);
}
/*
 当一个datatask转换为一个downloadtask以后会调用。
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask{
    
}
/*
 暂时忽略,这个是和数据流相关的。不管了
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
didBecomeStreamTask:(NSURLSessionStreamTask *)streamTask{

}
/*
 当data可以使用的时候,调用这个方法。我们可以在这里获取data。
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data{
    [self.data appendData:data];
    NSLog(@"具体数据在URLSession:(NSURLSession *)session task:(NSURLSessionTask *)taskdidCompleteWithError:(nullable NSError *)error处理");
}
/*
 允许我们在这里调用completionHandler缓存data,或者传入nil来禁止缓存
 */
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse * _Nullable cachedResponse))completionHandler{

}
//==================================NSURLSessionDownloadTask=================================
#pragma NSURLSessionDownloadTask
/*
    当一个下载task任务完成以后,这个方法会被调用。我们可以在这里移动或者复制download的数据
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location{
    NSString *path = location.absoluteString;
    UIImage *image = [[UIImage alloc]initWithData:[NSData dataWithContentsOfURL:location]];
    self.imageView.image = image;
    NSLog(@"数据下载完成以后,会保存在一个location的地方。%@",location);
}
/*
 获取下载进度
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
      didWriteData:(int64_t)bytesWritten
 totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite{
    NSLog(@"总得数据大小%lld----",bytesWritten);
}
/*
 重启一个下载任务(比如下载一半后停止然后过一点时间继续)。如果下载出错,`NSURLSessionDownloadTaskResumeData`里面包含重新开始下载的数据。
 */
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
 didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes{

}
@end
查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 26 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-11-10
个人主页被 748 人浏览